diff --git a/keystoneclient/base.py b/keystoneclient/base.py index 030afefb9..316b79e27 100644 --- a/keystoneclient/base.py +++ b/keystoneclient/base.py @@ -211,13 +211,12 @@ class Manager(object): return self.client.delete(url, **kwargs) def _update(self, url, body=None, response_key=None, method="PUT", - management=True, **kwargs): + **kwargs): methods = {"PUT": self.client.put, "POST": self.client.post, "PATCH": self.client.patch} try: resp, body = methods[method](url, body=body, - management=management, **kwargs) except KeyError: raise exceptions.ClientException(_("Invalid update method: %s") diff --git a/keystoneclient/httpclient.py b/keystoneclient/httpclient.py index 79b5e88c3..428a742ce 100644 --- a/keystoneclient/httpclient.py +++ b/keystoneclient/httpclient.py @@ -52,10 +52,11 @@ if not hasattr(urlparse, 'parse_qsl'): from keystoneclient import access +from keystoneclient import adapter from keystoneclient.auth import base from keystoneclient import baseclient from keystoneclient import exceptions -from keystoneclient.i18n import _, _LI, _LW +from keystoneclient.i18n import _, _LW from keystoneclient import session as client_session from keystoneclient import utils @@ -84,6 +85,51 @@ class _FakeRequestSession(object): return requests.request(*args, **kwargs) +class _KeystoneAdapter(adapter.LegacyJsonAdapter): + """A wrapper layer to interface keystoneclient with a session. + + An adapter provides a generic interface between a client and the session to + provide client specific defaults. This object is passed to the managers. + Keystoneclient managers have some additional requirements of variables that + they expect to be present on the passed object. + + Subclass the existing adapter to provide those values that keystoneclient + managers expect. + """ + + @property + def user_id(self): + """Best effort to retrieve the user_id from the plugin. + + Some managers rely on being able to get the currently authenticated + user id. This is a problem when we are trying to abstract away the + details of an auth plugin. + + For example changing a user's password can require access to the + currently authenticated user_id. + + Perform a best attempt to fetch this data. It will work in the legacy + case and with identity plugins and be None otherwise which is the same + as the historical behavior. + """ + # the identity plugin case + try: + return self.session.auth.get_access(self.session).user_id + except AttributeError: + pass + + # there is a case that we explicity allow (tested by our unit tests) + # that says you should be able to set the user_id on a legacy client + # and it should overwrite the one retrieved via authentication. If it's + # a legacy then self.session.auth is a client and we retrieve user_id. + try: + return self.session.auth.user_id + except AttributeError: + pass + + return None + + class HTTPClient(baseclient.Client, base.BaseAuthPlugin): version = None @@ -169,7 +215,6 @@ class HTTPClient(baseclient.Client, base.BaseAuthPlugin): self.project_domain_id = None self.project_domain_name = None - self.region_name = None self.auth_url = None self._endpoint = None self._management_url = None @@ -193,8 +238,8 @@ class HTTPClient(baseclient.Client, base.BaseAuthPlugin): self._management_url = self.auth_ref.management_url[0] self.auth_token_from_user = self.auth_ref.auth_token self.trust_id = self.auth_ref.trust_id - if self.auth_ref.has_service_catalog(): - self.region_name = self.auth_ref.service_catalog.region_name + if self.auth_ref.has_service_catalog() and not region_name: + region_name = self.auth_ref.service_catalog.region_name else: self.auth_ref = None @@ -251,8 +296,6 @@ class HTTPClient(baseclient.Client, base.BaseAuthPlugin): self.auth_token_from_user = None if endpoint: self._endpoint = endpoint.rstrip('/') - if region_name: - self.region_name = region_name self._auth_token = None if not session: @@ -264,6 +307,12 @@ class HTTPClient(baseclient.Client, base.BaseAuthPlugin): self.domain = '' self.debug_log = debug + self._adapter = _KeystoneAdapter(session, + service_type='identity', + interface='admin', + region_name=region_name, + version=self.version) + # keyring setup if use_keyring and keyring is None: _logger.warning(_LW('Failed to load keyring modules.')) @@ -396,7 +445,7 @@ class HTTPClient(baseclient.Client, base.BaseAuthPlugin): project_domain_name = project_domain_name or self.project_domain_name trust_id = trust_id or self.trust_id - region_name = region_name or self.region_name + region_name = region_name or self._adapter.region_name if not token: token = self.auth_token_from_user @@ -569,83 +618,110 @@ class HTTPClient(baseclient.Client, base.BaseAuthPlugin): def serialize(self, entity): return jsonutils.dumps(entity) - @staticmethod - def _decode_body(resp): - if resp.text: - try: - body_resp = jsonutils.loads(resp.text) - except (ValueError, TypeError): - body_resp = None - _logger.debug("Could not decode JSON from body: %s", - resp.text) - else: - _logger.debug("No body was returned.") - body_resp = None - - return body_resp - - def request(self, url, method, **kwargs): + def request(self, *args, **kwargs): """Send an http request with the specified characteristics. Wrapper around requests.request to handle tasks such as setting headers, JSON encoding/decoding, and error handling. + + .. warning:: + *DEPRECATED*: This function is no longer used. It was designed to + be used only by the managers and the managers now receive an + adapter so this function is no longer on the standard request path. """ - - try: - kwargs['json'] = kwargs.pop('body') - except KeyError: - pass - kwargs.setdefault('authenticated', False) - resp = super(HTTPClient, self).request(url, method, **kwargs) - return resp, self._decode_body(resp) + return self._adapter.request(*args, **kwargs) def _cs_request(self, url, method, management=True, **kwargs): """Makes an authenticated request to keystone endpoint by concatenating self.management_url and url and passing in method and any associated kwargs. """ - # NOTE(jamielennox): remember that if you use the legacy client mode - # (you create a client without a session) then this HTTPClient object - # is the auth plugin you are using. Values in the endpoint_filter may - # be ignored and you should look at get_endpoint to figure out what. - interface = 'admin' if management else 'public' - endpoint_filter = kwargs.setdefault('endpoint_filter', {}) - endpoint_filter.setdefault('service_type', 'identity') - endpoint_filter.setdefault('interface', interface) - - if self.version: - endpoint_filter.setdefault('version', self.version) - - if self.region_name: - endpoint_filter.setdefault('region_name', self.region_name) + # NOTE(jamielennox): This is deprecated and is no longer a part of the + # standard client request path. It now goes via the adapter instead. + if not management: + endpoint_filter = kwargs.setdefault('endpoint_filter', {}) + endpoint_filter.setdefault('interface', 'public') kwargs.setdefault('authenticated', None) - try: - return self.request(url, method, **kwargs) - except exceptions.MissingAuthPlugin: - _logger.info(_LI('Cannot get authenticated endpoint without an ' - 'auth plugin')) - raise exceptions.AuthorizationFailure( - _('Current authorization does not have a known management ' - 'url')) + return self.request(url, method, **kwargs) def get(self, url, **kwargs): + """Perform an authenticated GET request. + + This calls :py:meth:`.request()` with ``method`` set to ``GET`` and an + authentication token if one is available. + + .. warning:: + *DEPRECATED*: This function is no longer used. It was designed to + be used by the managers and the managers now receive an adapter so + this function is no longer on the standard request path. + """ return self._cs_request(url, 'GET', **kwargs) def head(self, url, **kwargs): + """Perform an authenticated HEAD request. + + This calls :py:meth:`.request()` with ``method`` set to ``HEAD`` and an + authentication token if one is available. + + .. warning:: + *DEPRECATED*: This function is no longer used. It was designed to + be used by the managers and the managers now receive an adapter so + this function is no longer on the standard request path. + """ return self._cs_request(url, 'HEAD', **kwargs) def post(self, url, **kwargs): + """Perform an authenticate POST request. + + This calls :py:meth:`.request()` with ``method`` set to ``POST`` and an + authentication token if one is available. + + .. warning:: + *DEPRECATED*: This function is no longer used. It was designed to + be used by the managers and the managers now receive an adapter so + this function is no longer on the standard request path. + """ return self._cs_request(url, 'POST', **kwargs) def put(self, url, **kwargs): + """Perform an authenticate PUT request. + + This calls :py:meth:`.request()` with ``method`` set to ``PUT`` and an + authentication token if one is available. + + .. warning:: + *DEPRECATED*: This function is no longer used. It was designed to + be used by the managers and the managers now receive an adapter so + this function is no longer on the standard request path. + """ return self._cs_request(url, 'PUT', **kwargs) def patch(self, url, **kwargs): + """Perform an authenticate PATCH request. + + This calls :py:meth:`.request()` with ``method`` set to ``PATCH`` and + an authentication token if one is available. + + .. warning:: + *DEPRECATED*: This function is no longer used. It was designed to + be used by the managers and the managers now receive an adapter so + this function is no longer on the standard request path. + """ return self._cs_request(url, 'PATCH', **kwargs) def delete(self, url, **kwargs): + """Perform an authenticate DELETE request. + + This calls :py:meth:`.request()` with ``method`` set to ``DELETE`` and + an authentication token if one is available. + + .. warning:: + *DEPRECATED*: This function is no longer used. It was designed to + be used by the managers and the managers now receive an adapter so + this function is no longer on the standard request path. + """ return self._cs_request(url, 'DELETE', **kwargs) # DEPRECATIONS: The following methods are no longer directly supported @@ -656,20 +732,40 @@ class HTTPClient(baseclient.Client, base.BaseAuthPlugin): 'timeout': None, 'verify_cert': 'verify'} + deprecated_adapter_variables = {'region_name': None} + def __getattr__(self, name): # FIXME(jamielennox): provide a proper deprecated warning try: var_name = self.deprecated_session_variables[name] except KeyError: - raise AttributeError(_("Unknown Attribute: %s") % name) + pass + else: + return getattr(self.session, var_name or name) - return getattr(self.session, var_name or name) + try: + var_name = self.deprecated_adapter_variables[name] + except KeyError: + pass + else: + return getattr(self._adapter, var_name or name) + + raise AttributeError(_("Unknown Attribute: %s") % name) def __setattr__(self, name, val): # FIXME(jamielennox): provide a proper deprecated warning try: var_name = self.deprecated_session_variables[name] except KeyError: - super(HTTPClient, self).__setattr__(name, val) + pass else: - setattr(self.session, var_name or name) + return setattr(self.session, var_name or name) + + try: + var_name = self.deprecated_adapter_variables[name] + except KeyError: + pass + else: + return setattr(self._adapter, var_name or name) + + super(HTTPClient, self).__setattr__(name, val) diff --git a/keystoneclient/tests/test_base.py b/keystoneclient/tests/test_base.py index 023d80742..52435b8e0 100644 --- a/keystoneclient/tests/test_base.py +++ b/keystoneclient/tests/test_base.py @@ -40,8 +40,8 @@ class BaseTest(utils.TestCase): auth_url='http://127.0.0.1:5000', endpoint='http://127.0.0.1:5000') - self.client.get = self.mox.CreateMockAnything() - self.client.get('/OS-KSADM/roles/1').AndRaise(AttributeError) + self.client._adapter.get = self.mox.CreateMockAnything() + self.client._adapter.get('/OS-KSADM/roles/1').AndRaise(AttributeError) self.mox.ReplayAll() f = roles.Role(self.client.roles, {'id': 1, 'name': 'Member'}) diff --git a/keystoneclient/v2_0/client.py b/keystoneclient/v2_0/client.py index dc790e83a..f7bf153e0 100644 --- a/keystoneclient/v2_0/client.py +++ b/keystoneclient/v2_0/client.py @@ -130,17 +130,19 @@ class Client(httpclient.HTTPClient): def __init__(self, **kwargs): """Initialize a new client for the Keystone v2.0 API.""" super(Client, self).__init__(**kwargs) - self.endpoints = endpoints.EndpointManager(self) - self.extensions = extensions.ExtensionManager(self) - self.roles = roles.RoleManager(self) - self.services = services.ServiceManager(self) - self.tokens = tokens.TokenManager(self) - self.users = users.UserManager(self, self.roles) - self.tenants = tenants.TenantManager(self, self.roles, self.users) + self.endpoints = endpoints.EndpointManager(self._adapter) + self.extensions = extensions.ExtensionManager(self._adapter) + self.roles = roles.RoleManager(self._adapter) + self.services = services.ServiceManager(self._adapter) + self.tokens = tokens.TokenManager(self._adapter) + self.users = users.UserManager(self._adapter, self.roles) + + self.tenants = tenants.TenantManager(self._adapter, + self.roles, self.users) # extensions - self.ec2 = ec2.CredentialsManager(self) + self.ec2 = ec2.CredentialsManager(self._adapter) # DEPRECATED: if session is passed then we go to the new behaviour of # authenticating on the first required call. diff --git a/keystoneclient/v2_0/users.py b/keystoneclient/v2_0/users.py index df488f541..11e06f3ef 100644 --- a/keystoneclient/v2_0/users.py +++ b/keystoneclient/v2_0/users.py @@ -78,7 +78,7 @@ class UserManager(base.ManagerWithFind): return self._update("/OS-KSCRUD/users/%s" % self.api.user_id, params, response_key="access", method="PATCH", - management=False, + endpoint_filter={'interface': 'public'}, log=False) def update_tenant(self, user, tenant): diff --git a/keystoneclient/v3/client.py b/keystoneclient/v3/client.py index 7d3f0fe67..2c1f666cf 100644 --- a/keystoneclient/v3/client.py +++ b/keystoneclient/v3/client.py @@ -169,23 +169,26 @@ EndpointPolicyManager` """Initialize a new client for the Keystone v3 API.""" super(Client, self).__init__(**kwargs) - self.credentials = credentials.CredentialManager(self) - self.endpoint_filter = endpoint_filter.EndpointFilterManager(self) - self.endpoint_policy = endpoint_policy.EndpointPolicyManager(self) - self.endpoints = endpoints.EndpointManager(self) - self.domains = domains.DomainManager(self) - self.federation = federation.FederationManager(self) - self.groups = groups.GroupManager(self) - self.oauth1 = oauth1.create_oauth_manager(self) - self.policies = policies.PolicyManager(self) - self.projects = projects.ProjectManager(self) - self.regions = regions.RegionManager(self) - self.role_assignments = role_assignments.RoleAssignmentManager(self) - self.roles = roles.RoleManager(self) - self.services = services.ServiceManager(self) - self.tokens = tokens.TokenManager(self) - self.trusts = trusts.TrustManager(self) - self.users = users.UserManager(self) + self.credentials = credentials.CredentialManager(self._adapter) + self.endpoint_filter = endpoint_filter.EndpointFilterManager( + self._adapter) + self.endpoint_policy = endpoint_policy.EndpointPolicyManager( + self._adapter) + self.endpoints = endpoints.EndpointManager(self._adapter) + self.domains = domains.DomainManager(self._adapter) + self.federation = federation.FederationManager(self._adapter) + self.groups = groups.GroupManager(self._adapter) + self.oauth1 = oauth1.create_oauth_manager(self._adapter) + self.policies = policies.PolicyManager(self._adapter) + self.projects = projects.ProjectManager(self._adapter) + self.regions = regions.RegionManager(self._adapter) + self.role_assignments = ( + role_assignments.RoleAssignmentManager(self._adapter)) + self.roles = roles.RoleManager(self._adapter) + self.services = services.ServiceManager(self._adapter) + self.tokens = tokens.TokenManager(self._adapter) + self.trusts = trusts.TrustManager(self._adapter) + self.users = users.UserManager(self._adapter) # DEPRECATED: if session is passed then we go to the new behaviour of # authenticating on the first required call. diff --git a/keystoneclient/v3/contrib/endpoint_filter.py b/keystoneclient/v3/contrib/endpoint_filter.py index 3e3b7ef38..0da79b81b 100644 --- a/keystoneclient/v3/contrib/endpoint_filter.py +++ b/keystoneclient/v3/contrib/endpoint_filter.py @@ -15,6 +15,8 @@ from keystoneclient import base from keystoneclient import exceptions from keystoneclient.i18n import _ +from keystoneclient.v3 import endpoints +from keystoneclient.v3 import projects class EndpointFilterManager(base.Manager): @@ -72,8 +74,8 @@ class EndpointFilterManager(base.Manager): base_url = self._build_base_url(project=project) return super(EndpointFilterManager, self)._list( base_url, - self.client.endpoints.collection_key, - obj_class=self.client.endpoints.resource_class) + endpoints.EndpointManager.collection_key, + obj_class=endpoints.EndpointManager.resource_class) def list_projects_for_endpoint(self, endpoint): """List all projects for a given endpoint.""" @@ -83,5 +85,5 @@ class EndpointFilterManager(base.Manager): base_url = self._build_base_url(endpoint=endpoint) return super(EndpointFilterManager, self)._list( base_url, - self.client.projects.collection_key, - obj_class=self.client.projects.resource_class) + projects.ProjectManager.collection_key, + obj_class=projects.ProjectManager.resource_class) diff --git a/keystoneclient/v3/contrib/endpoint_policy.py b/keystoneclient/v3/contrib/endpoint_policy.py index c473ad602..24148c1ac 100644 --- a/keystoneclient/v3/contrib/endpoint_policy.py +++ b/keystoneclient/v3/contrib/endpoint_policy.py @@ -14,6 +14,7 @@ from keystoneclient import base from keystoneclient.i18n import _ +from keystoneclient.v3 import endpoints from keystoneclient.v3 import policies @@ -150,5 +151,5 @@ class EndpointPolicyManager(base.Manager): 'ext_name': self.OS_EP_POLICY_EXT} return self._list( url, - self.client.endpoints.collection_key, - obj_class=self.client.endpoints.resource_class) + endpoints.EndpointManager.collection_key, + obj_class=endpoints.EndpointManager.resource_class) diff --git a/keystoneclient/v3/contrib/oauth1/access_tokens.py b/keystoneclient/v3/contrib/oauth1/access_tokens.py index ea27797b2..12b0c6bc6 100644 --- a/keystoneclient/v3/contrib/oauth1/access_tokens.py +++ b/keystoneclient/v3/contrib/oauth1/access_tokens.py @@ -13,6 +13,7 @@ from __future__ import unicode_literals +from keystoneclient import auth from keystoneclient import base from keystoneclient.v3.contrib.oauth1 import utils @@ -39,8 +40,9 @@ class AccessTokenManager(base.CrudManager): resource_owner_secret=request_secret, signature_method=oauth1.SIGNATURE_HMAC, verifier=verifier) - url = self.client.auth_url.rstrip("/") + endpoint - url, headers, body = oauth_client.sign(url, http_method='POST') + url = self.api.get_endpoint(interface=auth.AUTH_INTERFACE).rstrip('/') + url, headers, body = oauth_client.sign(url + endpoint, + http_method='POST') resp, body = self.client.post(endpoint, headers=headers) token = utils.get_oauth_token_from_body(resp.content) return self.resource_class(self, token) diff --git a/keystoneclient/v3/contrib/oauth1/request_tokens.py b/keystoneclient/v3/contrib/oauth1/request_tokens.py index 27d6d34fc..bc30ce08d 100644 --- a/keystoneclient/v3/contrib/oauth1/request_tokens.py +++ b/keystoneclient/v3/contrib/oauth1/request_tokens.py @@ -15,6 +15,7 @@ from __future__ import unicode_literals from six.moves.urllib import parse as urlparse +from keystoneclient import auth from keystoneclient import base from keystoneclient.v3.contrib.oauth1 import utils @@ -62,8 +63,9 @@ class RequestTokenManager(base.CrudManager): client_secret=consumer_secret, signature_method=oauth1.SIGNATURE_HMAC, callback_uri="oob") - url = self.client.auth_url.rstrip("/") + endpoint - url, headers, body = oauth_client.sign(url, http_method='POST', + url = self.api.get_endpoint(interface=auth.AUTH_INTERFACE).rstrip("/") + url, headers, body = oauth_client.sign(url + endpoint, + http_method='POST', headers=headers) resp, body = self.client.post(endpoint, headers=headers) token = utils.get_oauth_token_from_body(resp.content) diff --git a/keystoneclient/v3/users.py b/keystoneclient/v3/users.py index 3343d50cd..2e20ede3c 100644 --- a/keystoneclient/v3/users.py +++ b/keystoneclient/v3/users.py @@ -158,8 +158,8 @@ class UserManager(base.CrudManager): base_url = '/users/%s/password' % self.api.user_id - return self._update(base_url, params, method='POST', management=False, - log=False) + return self._update(base_url, params, method='POST', log=False, + endpoint_filter={'interface': 'public'}) def add_to_group(self, user, group): self._require_user_and_group(user, group)