Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make returned responses more consistent #537

Merged
merged 6 commits into from
Mar 28, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/advanced_usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ Custom Requests / HTTP Adapter

.. versionadded:: 0.6.2

Calls to the `requests module`_. (which provides the methods hvac utilizes to send HTTP/HTTPS request to Vault instances) were extracted from the :class:`Client <hvac.v1.Client>` class and moved to a newly added :meth:`hvac.adapters` module. The :class:`Client <hvac.v1.Client>` class itself defaults to an instance of the :class:`Request <hvac.adapters.Request>` class for its :attr:`_adapter <hvac.v1.Client._adapter>` private attribute attribute if no adapter argument is provided to its :meth:`constructor <hvac.v1.Client.__init__>`. This attribute provides an avenue for modifying the manner in which hvac completes request. To enable this type of customization, implement a class of type :meth:`hvac.adapters.Adapter`, override its abstract methods, and pass this custom class to the adapter argument of the :meth:`Client constructor <hvac.v1.Client.__init__>`
Calls to the `requests module`_. (which provides the methods hvac utilizes to send HTTP/HTTPS request to Vault instances) were extracted from the :class:`Client <hvac.v1.Client>` class and moved to a newly added :meth:`hvac.adapters` module. The :class:`Client <hvac.v1.Client>` class itself defaults to an instance of the :class:`JSONAdapter <hvac.adapters.JSONAdapter>` class for its :attr:`_adapter <hvac.v1.Client._adapter>` private attribute attribute if no adapter argument is provided to its :meth:`constructor <hvac.v1.Client.__init__>`. This attribute provides an avenue for modifying the manner in which hvac completes request. To enable this type of customization, implement a class of type :meth:`hvac.adapters.Adapter`, override its abstract methods, and pass this custom class to the adapter argument of the :meth:`Client constructor <hvac.v1.Client.__init__>`

.. _requests module: http://requests.readthedocs.io/en/master/

Expand Down
89 changes: 81 additions & 8 deletions hvac/adapters.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ class Adapter(object):
__metaclass__ = ABCMeta

def __init__(self, base_uri=DEFAULT_BASE_URI, token=None, cert=None, verify=True, timeout=30, proxies=None,
allow_redirects=True, session=None, namespace=None):
allow_redirects=True, session=None, namespace=None, ignore_exceptions=False):
"""Create a new request adapter instance.

:param base_uri: Base URL for the Vault instance being addressed.
Expand All @@ -42,6 +42,9 @@ def __init__(self, base_uri=DEFAULT_BASE_URI, token=None, cert=None, verify=True
:type session: request.Session
:param namespace: Optional Vault Namespace.
:type namespace: str
:param ignore_exceptions: If True, _always_ return the response object for a given request. I.e., don't raise an exception
based on response status code, etc.
:type ignore_exceptions: bool
"""
if not session:
session = requests.Session()
Expand All @@ -51,6 +54,7 @@ def __init__(self, base_uri=DEFAULT_BASE_URI, token=None, cert=None, verify=True
self.namespace = namespace
self.session = session
self.allow_redirects = allow_redirects
self.ignore_exceptions = ignore_exceptions

self._kwargs = {
'cert': cert,
Expand Down Expand Up @@ -171,13 +175,23 @@ def login(self, url, use_token=True, **kwargs):
:return: The response of the auth request.
:rtype: requests.Response
"""
response = self.post(url, **kwargs).json()
response = self.post(url, **kwargs)

if use_token:
self.token = response['auth']['client_token']
self.token = self.get_login_token(response)

return response

@abstractmethod
def get_login_token(self, response):
"""Extracts the client token from a login response.

:param response: The response object returned by the login method.
:return: A client token.
:rtype: str
"""
return NotImplementedError

@utils.deprecated_method(
to_be_removed_in_version='0.9.0',
new_method=login,
Expand Down Expand Up @@ -211,8 +225,23 @@ def request(self, method, url, headers=None, raise_exception=True, **kwargs):
raise NotImplementedError


class Request(Adapter):
"""The Request adapter class"""
class RawAdapter(Adapter):
"""
The RawAdapter adapter class.
This adapter adds Vault-specific headers as required and optionally raises exceptions on errors,
but always returns Response objects for requests.
"""

def get_login_token(self, response):
"""Extracts the client token from a login response.

:param response: The response object returned by the login method.
:type response: requests.Response
:return: A client token.
:rtype: str
"""
response_json = response.json()
return response_json['auth']['client_token']

def request(self, method, url, headers=None, raise_exception=True, **kwargs):
"""Main method for routing HTTP requests to the configured Vault base_uri.
Expand All @@ -232,7 +261,7 @@ def request(self, method, url, headers=None, raise_exception=True, **kwargs):
:return: The response of the request.
:rtype: requests.Response
"""
if '//' in url:
while '//' in url:
# Vault CLI treats a double forward slash ('//') as a single forward slash for a given path.
# To avoid issues with the requests module's redirection logic, we perform the same translation here.
url = url.replace('//', '/')
Expand Down Expand Up @@ -263,12 +292,56 @@ def request(self, method, url, headers=None, raise_exception=True, **kwargs):
**_kwargs
)

if raise_exception and 400 <= response.status_code < 600:
if not response.ok and (raise_exception and not self.ignore_exceptions):
text = errors = None
if response.headers.get('Content-Type') == 'application/json':
errors = response.json().get('errors')
try:
errors = response.json().get('errors')
except Exception:
pass
if errors is None:
text = response.text
utils.raise_for_error(response.status_code, text, errors=errors)

return response


class JSONAdapter(RawAdapter):
"""
The JSONAdapter adapter class.
This adapter works just like the RawAdapter adapter except that HTTP 200 responses are returned as JSON dicts.
All non-200 responses are returned as Response objects.
"""

def get_login_token(self, response):
"""Extracts the client token from a login response.

:param response: The response object returned by the login method.
:type response: dict | requests.Response
:return: A client token.
:rtype: str
"""
return response['auth']['client_token']

def request(self, *args, **kwargs):
"""Main method for routing HTTP requests to the configured Vault base_uri.

:param args: Positional arguments to pass to RawAdapter.request.
:type args: list
:param kwargs: Keyword arguments to pass to RawAdapter.request.
:type kwargs: dict
:return: Dict on HTTP 200 with JSON body, otherwise the response object.
:rtype: dict | requests.Response
"""
response = super(JSONAdapter, self).request(*args, **kwargs)
if response.status_code == 200:
try:
return response.json()
except ValueError:
pass

return response


# Retaining the legacy name
Request = RawAdapter
28 changes: 14 additions & 14 deletions hvac/api/auth_methods/aws.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ def read_config(self, mount_point=AWS_DEFAULT_MOUNT_POINT):
response = self._adapter.get(
url=api_path,
)
return response.json().get('data')
return response.get('data')

def delete_config(self, mount_point=AWS_DEFAULT_MOUNT_POINT):
"""Deletes the previously configured AWS access credentials
Expand Down Expand Up @@ -178,7 +178,7 @@ def read_identity_integration(self, mount_point=AWS_DEFAULT_MOUNT_POINT):
response = self._adapter.get(
url=api_path,
)
return response.json().get('data')
return response.get('data')

def create_certificate_configuration(self, cert_name, aws_public_cert, document_type=None, mount_point=AWS_DEFAULT_MOUNT_POINT):
"""Registers an AWS public key to be used to verify the instance identity documents
Expand Down Expand Up @@ -233,7 +233,7 @@ def read_certificate_configuration(self, cert_name, mount_point=AWS_DEFAULT_MOUN
response = self._adapter.get(
url=api_path,
)
return response.json().get('data')
return response.get('data')

def delete_certificate_configuration(self, cert_name, mount_point=AWS_DEFAULT_MOUNT_POINT):
"""Removes the previously configured AWS public key
Expand Down Expand Up @@ -266,7 +266,7 @@ def list_certificate_configurations(self, mount_point=AWS_DEFAULT_MOUNT_POINT):
response = self._adapter.list(
url=api_path,
)
return response.json().get('data')
return response.get('data')

def create_sts_role(self, account_id, sts_role, mount_point=AWS_DEFAULT_MOUNT_POINT):
""" Allows the explicit association of STS roles to satellite AWS accounts (i.e. those which are not the
Expand Down Expand Up @@ -304,7 +304,7 @@ def read_sts_role(self, account_id, mount_point=AWS_DEFAULT_MOUNT_POINT):
response = self._adapter.get(
url=api_path,
)
return response.json().get('data')
return response.get('data')

def list_sts_roles(self, mount_point=AWS_DEFAULT_MOUNT_POINT):
"""Lists all the AWS Account IDs for which an STS role is registered
Expand All @@ -316,7 +316,7 @@ def list_sts_roles(self, mount_point=AWS_DEFAULT_MOUNT_POINT):
response = self._adapter.list(
url=api_path
)
return response.json().get('data')
return response.get('data')

def delete_sts_role(self, account_id, mount_point=AWS_DEFAULT_MOUNT_POINT):
"""Deletes a previously configured AWS account/STS role association
Expand Down Expand Up @@ -359,7 +359,7 @@ def read_identity_whitelist_tidy(self, mount_point=AWS_DEFAULT_MOUNT_POINT):
response = self._adapter.get(
url=api_path
)
return response.json().get('data')
return response.get('data')

def delete_identity_whitelist_tidy(self, mount_point=AWS_DEFAULT_MOUNT_POINT):
"""Deletes the previously configured periodic whitelist tidying settings
Expand Down Expand Up @@ -401,7 +401,7 @@ def read_role_tag_blacklist_tidy(self, mount_point=AWS_DEFAULT_MOUNT_POINT):
response = self._adapter.get(
url=api_path
)
return response.json().get('data')
return response.get('data')

def delete_role_tag_blacklist_tidy(self, mount_point=AWS_DEFAULT_MOUNT_POINT):
"""Deletes the previously configured periodic blacklist tidying settings
Expand Down Expand Up @@ -499,7 +499,7 @@ def read_role(self, role, mount_point=AWS_DEFAULT_MOUNT_POINT):
response = self._adapter.get(
url=api_path
)
return response.json().get('data')
return response.get('data')

def list_roles(self, mount_point=AWS_DEFAULT_MOUNT_POINT):
"""Lists all the roles that are registered with the method
Expand All @@ -511,7 +511,7 @@ def list_roles(self, mount_point=AWS_DEFAULT_MOUNT_POINT):
response = self._adapter.list(
url=api_path,
)
return response.json().get('data')
return response.get('data')

def delete_role(self, role, mount_point=AWS_DEFAULT_MOUNT_POINT):
"""Deletes the previously registered role
Expand Down Expand Up @@ -650,7 +650,7 @@ def read_role_tag_blacklist(self, role_tag, mount_point=AWS_DEFAULT_MOUNT_POINT)
response = self._adapter.get(
url=api_path
)
return response.json().get('data')
return response.get('data')

def list_blacklist_tags(self, mount_point=AWS_DEFAULT_MOUNT_POINT):
"""Lists all the role tags that are blacklisted
Expand All @@ -662,7 +662,7 @@ def list_blacklist_tags(self, mount_point=AWS_DEFAULT_MOUNT_POINT):
response = self._adapter.list(
url=api_path,
)
return response.json().get('data')
return response.get('data')

def delete_blacklist_tags(self, role_tag, mount_point=AWS_DEFAULT_MOUNT_POINT):
"""Deletes a blacklisted role tag
Expand Down Expand Up @@ -703,7 +703,7 @@ def read_identity_whitelist(self, instance_id, mount_point=AWS_DEFAULT_MOUNT_POI
response = self._adapter.get(
url=api_path
)
return response.json().get('data')
return response.get('data')

def list_identity_whitelist(self, mount_point=AWS_DEFAULT_MOUNT_POINT):
"""Lists all the instance IDs that are in the whitelist of successful logins
Expand All @@ -715,7 +715,7 @@ def list_identity_whitelist(self, mount_point=AWS_DEFAULT_MOUNT_POINT):
response = self._adapter.list(
url=api_path,
)
return response.json().get('data')
return response.get('data')

def delete_identity_whitelist_entries(self, instance_id, mount_point=AWS_DEFAULT_MOUNT_POINT):
"""Deletes a cache of the successful login from an instance
Expand Down
9 changes: 4 additions & 5 deletions hvac/api/auth_methods/azure.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ def read_config(self, mount_point=DEFAULT_MOUNT_POINT):
response = self._adapter.get(
url=api_path,
)
return response.json().get('data')
return response.get('data')

def delete_config(self, mount_point=DEFAULT_MOUNT_POINT):
"""Delete the previously configured Azure config and credentials.
Expand Down Expand Up @@ -194,7 +194,7 @@ def read_role(self, name, mount_point=DEFAULT_MOUNT_POINT):
response = self._adapter.get(
url=api_path,
)
return response.json().get('data')
return response.get('data')

def list_roles(self, mount_point=DEFAULT_MOUNT_POINT):
"""List all the roles that are registered with the plugin.
Expand All @@ -212,7 +212,7 @@ def list_roles(self, mount_point=DEFAULT_MOUNT_POINT):
response = self._adapter.list(
url=api_path
)
return response.json().get('data')
return response.get('data')

def delete_role(self, name, mount_point=DEFAULT_MOUNT_POINT):
"""Delete the previously registered role.
Expand Down Expand Up @@ -285,9 +285,8 @@ def login(self, role, jwt, subscription_id=None, resource_group_name=None, vm_na
})
)
api_path = utils.format_url('/v1/auth/{mount_point}/login', mount_point=mount_point)
response = self._adapter.login(
return self._adapter.login(
url=api_path,
use_token=use_token,
json=params,
)
return response
9 changes: 4 additions & 5 deletions hvac/api/auth_methods/gcp.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ def read_config(self, mount_point=DEFAULT_MOUNT_POINT):
response = self._adapter.get(
url=api_path,
)
return response.json().get('data')
return response.get('data')

def delete_config(self, mount_point=DEFAULT_MOUNT_POINT):
"""Delete all GCP configuration data. This operation is idempotent.
Expand Down Expand Up @@ -334,7 +334,7 @@ def read_role(self, name, mount_point=DEFAULT_MOUNT_POINT):
url=api_path,
json=params,
)
return response.json().get('data')
return response.get('data')

def list_roles(self, mount_point=DEFAULT_MOUNT_POINT):
"""List all the roles that are registered with the plugin.
Expand All @@ -352,7 +352,7 @@ def list_roles(self, mount_point=DEFAULT_MOUNT_POINT):
response = self._adapter.list(
url=api_path,
)
return response.json().get('data')
return response.get('data')

def delete_role(self, role, mount_point=DEFAULT_MOUNT_POINT):
"""Delete the previously registered role.
Expand Down Expand Up @@ -408,9 +408,8 @@ def login(self, role, jwt, use_token=True, mount_point=DEFAULT_MOUNT_POINT):
'jwt': jwt,
}
api_path = utils.format_url('/v1/auth/{mount_point}/login', mount_point=mount_point)
response = self._adapter.login(
return self._adapter.login(
url=api_path,
use_token=use_token,
json=params,
)
return response
9 changes: 3 additions & 6 deletions hvac/api/auth_methods/github.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,7 @@ def read_configuration(self, mount_point=DEFAULT_MOUNT_POINT):
'/v1/auth/{mount_point}/config',
mount_point=mount_point,
)
response = self._adapter.get(url=api_path)
return response.json()
return self._adapter.get(url=api_path)

def map_team(self, team_name, policies=None, mount_point=DEFAULT_MOUNT_POINT):
"""Map a list of policies to a team that exists in the configured GitHub organization.
Expand Down Expand Up @@ -133,8 +132,7 @@ def read_team_mapping(self, team_name, mount_point=DEFAULT_MOUNT_POINT):
mount_point=mount_point,
team_name=team_name,
)
response = self._adapter.get(url=api_path)
return response.json()
return self._adapter.get(url=api_path)

def map_user(self, user_name, policies=None, mount_point=DEFAULT_MOUNT_POINT):
"""Map a list of policies to a specific GitHub user exists in the configured organization.
Expand Down Expand Up @@ -195,8 +193,7 @@ def read_user_mapping(self, user_name, mount_point=DEFAULT_MOUNT_POINT):
mount_point=mount_point,
user_name=user_name,
)
response = self._adapter.get(url=api_path)
return response.json()
return self._adapter.get(url=api_path)

def login(self, token, use_token=True, mount_point=DEFAULT_MOUNT_POINT):
"""Login using GitHub access token.
Expand Down