From b4cb4a17e9a31d8487418762aede196daa7ba957 Mon Sep 17 00:00:00 2001 From: Jamie Lennox Date: Wed, 22 Oct 2014 16:14:29 +0200 Subject: [PATCH] Split identity server into v2 and v3 We make the IdentityServer object an abstraction layer between v2 and v3 APIs as the whole object. By doing this we don't have to check at the beginning of every function whether to use v2 or v3. This will be important when using auth plugins because the expected workflow is just to issue the calls and see which ones fail with EndpointNotFound and so we would have to reorder the code to possibly issue each call twice. We need the additional test discovery entries as discovery now happens when you instantiate the IdentityServer object - not when you use it. This will have no practical difference, it's just that in testing we sometimes create the object to read values from it. Change-Id: Ie05d99946c2b1127992cd9a083a2068a6e80398b --- keystonemiddleware/auth_token.py | 235 +++++++++--------- .../tests/test_auth_token_middleware.py | 5 +- 2 files changed, 122 insertions(+), 118 deletions(-) diff --git a/keystonemiddleware/auth_token.py b/keystonemiddleware/auth_token.py index 1f1eba67..974e5906 100644 --- a/keystonemiddleware/auth_token.py +++ b/keystonemiddleware/auth_token.py @@ -353,7 +353,6 @@ _OPTS = [ _AUTHTOKEN_GROUP = 'keystone_authtoken' CONF = cfg.CONF CONF.register_opts(_OPTS, group=_AUTHTOKEN_GROUP) -_LIST_OF_VERSIONS_TO_ATTEMPT = ['v3.0', 'v2.0'] _HEADER_TEMPLATE = { 'X%s-Domain-Id': 'domain_id', @@ -611,12 +610,9 @@ class _AuthTokenPlugin(auth.BaseAuthPlugin): # NOTE(jamielennox): for backwards compatibility here we don't # actually use the URL from discovery we hack it up instead. :( - # NOTE(jamielennox): matching the string version here matches the value - # from _LIST_OF_VERSIONS_TO_ATTEMPT. We can use the tuple only match - # in the future. - if version == 'v2.0' or version[0] == 2: + if version[0] == 2: return '%s/v2.0' % self._identity_uri - elif version == 'v3.0' or version[0] == 3: + elif version[0] == 3: return '%s/v3' % self._identity_uri # NOTE(jamielennox): This plugin will only get called from auth_token @@ -732,7 +728,7 @@ class AuthProtocol(object): self._include_service_catalog = self._conf_get( 'include_service_catalog') - self._identity_server = self._identity_server_factory() + self._identity_server_obj = None # signing self._signing_dirname = self._conf_get('signing_dir') @@ -1321,48 +1317,77 @@ class AuthProtocol(object): self._signing_ca_file_name, self._identity_server.fetch_ca_cert()) - def _identity_server_factory(self): - # NOTE(jamielennox): Loading Session here should be exactly the - # same as calling Session.load_from_conf_options(CONF, GROUP) - # however we can't do that because we have to use _conf_get to support - # the paste.ini options. - sess = session.Session.construct(dict( - cert=self._conf_get('certfile'), - key=self._conf_get('keyfile'), - cacert=self._conf_get('cafile'), - insecure=self._conf_get('insecure'), - timeout=self._conf_get('http_connect_timeout') - )) + @property + def _identity_server(self): + if not self._identity_server_obj: + # NOTE(jamielennox): Loading Session here should be exactly the + # same as calling Session.load_from_conf_options(CONF, GROUP) + # however we can't do that because we have to use _conf_get to + # support the paste.ini options. + sess = session.Session.construct(dict( + cert=self._conf_get('certfile'), + key=self._conf_get('keyfile'), + cacert=self._conf_get('cafile'), + insecure=self._conf_get('insecure'), + timeout=self._conf_get('http_connect_timeout') + )) - # NOTE(jamielennox): Loading AuthTokenPlugin here should be exactly the - # same as calling _AuthTokenPlugin.load_from_conf_options(CONF, GROUP) - # however we can't do that because we have to use _conf_get to support - # the paste.ini options. - auth_plugin = _AuthTokenPlugin.load_from_options( - auth_host=self._conf_get('auth_host'), - auth_port=int(self._conf_get('auth_port')), - auth_protocol=self._conf_get('auth_protocol'), - auth_admin_prefix=self._conf_get('auth_admin_prefix'), - admin_user=self._conf_get('admin_user'), - admin_password=self._conf_get('admin_password'), - admin_tenant_name=self._conf_get('admin_tenant_name'), - admin_token=self._conf_get('admin_token'), - identity_uri=self._conf_get('identity_uri'), - log=self._LOG) + # NOTE(jamielennox): Loading AuthTokenPlugin here should be exactly + # the same as calling _AuthTokenPlugin.load_from_conf_options(CONF, + # GROUP) however we can't do that because we have to use _conf_get + # to support the paste.ini options. + auth_plugin = _AuthTokenPlugin.load_from_options( + auth_host=self._conf_get('auth_host'), + auth_port=int(self._conf_get('auth_port')), + auth_protocol=self._conf_get('auth_protocol'), + auth_admin_prefix=self._conf_get('auth_admin_prefix'), + admin_user=self._conf_get('admin_user'), + admin_password=self._conf_get('admin_password'), + admin_tenant_name=self._conf_get('admin_tenant_name'), + admin_token=self._conf_get('admin_token'), + identity_uri=self._conf_get('identity_uri'), + log=self._LOG) - adap = adapter.Adapter( - sess, - auth=auth_plugin, - service_type='identity', - interface='admin', - connect_retries=self._conf_get('http_request_max_retries')) + adap = adapter.Adapter( + sess, + auth=auth_plugin, + service_type='identity', + interface='admin', + connect_retries=self._conf_get('http_request_max_retries')) - return _IdentityServer( - self._LOG, - adap, - include_service_catalog=self._include_service_catalog, - auth_uri=self._conf_get('auth_uri'), - auth_version=self._conf_get('auth_version')) + server_class = self._get_server_class(adap) + adap.version = server_class.AUTH_VERSION + + self._identity_server_obj = server_class( + self._LOG, + adap, + include_service_catalog=self._include_service_catalog, + auth_uri=self._conf_get('auth_uri')) + + return self._identity_server_obj + + def _get_server_class(self, adap): + # FIXME(jamielennox): Checking string equality is bad, but consistent + # with the existing code. Fix this to better handle selecting v3. + auth_version = self._conf_get('auth_version') + + if auth_version == 'v3.0': + return _V3IdentityServer + elif auth_version: + return _V2IdentityServer + + for klass in _VERSIONS_TO_ATTEMPT: + if adap.get_endpoint(version=klass.AUTH_VERSION): + msg = _LI('Auth Token confirmed use of %s apis') + self._LOG.info(msg, auth_version) + return klass + + versions = ['v%d.%d' % s.AUTH_VERSION for s in _VERSIONS_TO_ATTEMPT] + self._LOG.error(_LE('No attempted versions [%s] supported by server') % + ', '.join(versions)) + + msg = _('No compatible apis supported by server') + raise ServiceError(msg) def _token_cache_factory(self): security_strategy = self._conf_get('memcache_security_strategy') @@ -1449,7 +1474,7 @@ class _MemcacheClientPool(object): class _IdentityServer(object): - """Operations on the Identity API server. + """Base class for operations on the Identity API server. The auth_token middleware needs to communicate with the Identity API server to validate UUID tokens, fetch the revocation list, signing certificates, @@ -1457,12 +1482,13 @@ class _IdentityServer(object): operations. """ - def __init__(self, log, adap, include_service_catalog=None, - auth_uri=None, auth_version=None): + + AUTH_VERSION = None + + def __init__(self, log, adap, include_service_catalog=None, auth_uri=None): self._LOG = log self._adapter = adap self._include_service_catalog = include_service_catalog - self._req_auth_version = auth_version if auth_uri is None: self._LOG.warning( @@ -1481,7 +1507,6 @@ class _IdentityServer(object): auth_uri = urllib.parse.urljoin(auth_uri, '/').rstrip('/') self.auth_uri = auth_uri - self._auth_version = None def verify_token(self, user_token, retry=True): """Authenticate user token with identity server. @@ -1497,31 +1522,8 @@ class _IdentityServer(object): """ user_token = _safe_quote(user_token) - # Determine the highest api version we can use. - if not self._auth_version: - self._auth_version = self._choose_api_version() - - headers = {} - - if self._auth_version == 'v3.0': - headers['X-Subject-Token'] = user_token - version = (3, 0) - path = '/auth/tokens' - if not self._include_service_catalog: - # NOTE(gyee): only v3 API support this option - path = path + '?nocatalog' - - else: - version = (2, 0) - path = '/tokens/%s' % user_token - try: - response, data = self._json_request( - 'GET', - path, - authenticated=True, - endpoint_filter={'version': version}, - headers=headers) + response, data = self._do_verify_token(user_token) except exceptions.NotFound as e: self._LOG.warn(_LW('Authorization failed for token')) self._LOG.warn(_LW('Identity response: %s') % e.response.text) @@ -1563,31 +1565,6 @@ class _IdentityServer(object): def fetch_ca_cert(self): return self._fetch_cert_file('ca') - def _choose_api_version(self): - """Determine the api version that we should use.""" - - # If the configuration specifies an auth_version we will just - # assume that is correct and use it. We could, of course, check - # that this version is supported by the server, but in case - # there are some problems in the field, we want as little code - # as possible in the way of letting auth_token talk to the - # server. - if self._req_auth_version: - self._LOG.info(_LI('Auth Token proceeding with requested %s apis'), - self._req_auth_version) - return self._req_auth_version - - for auth_version in _LIST_OF_VERSIONS_TO_ATTEMPT: - if self._adapter.get_endpoint(version=auth_version): - self._LOG.info(_LI('Auth Token confirmed use of %s apis'), - auth_version) - return auth_version - - self._LOG.error(_LE('No attempted versions [%s] supported by server') % - ', '.join(_LIST_OF_VERSIONS_TO_ATTEMPT)) - msg = _('No compatible apis supported by server') - raise ServiceError(msg) - def _json_request(self, method, path, **kwargs): """HTTP request helper used to make json requests. @@ -1612,24 +1589,8 @@ class _IdentityServer(object): return response, data def _fetch_cert_file(self, cert_type): - if not self._auth_version: - self._auth_version = self._choose_api_version() - - version = None - - if self._auth_version == 'v3.0': - if cert_type == 'signing': - cert_type = 'certificates' - path = '/OS-SIMPLE-CERT/' + cert_type - version = (3, 0) - else: - path = '/certificates/' + cert_type - version = (2, 0) - try: - response = self._adapter.get( - path, authenticated=False, - endpoint_filter={'version': version}) + response = self._do_fetch_cert_file(cert_type) except exceptions.HTTPError as e: raise exceptions.CertificateConfigError(e.details) if response.status_code != 200: @@ -1637,6 +1598,42 @@ class _IdentityServer(object): return response.text +class _V2IdentityServer(_IdentityServer): + + AUTH_VERSION = (2, 0) + + def _do_verify_token(self, user_token): + return self._json_request('GET', + '/tokens/%s' % user_token, + authenticated=True) + + def _do_fetch_cert_file(self, cert_type): + return self._adapter.get('/certificates/%s' % cert_type, + authenticated=False) + + +class _V3IdentityServer(_IdentityServer): + + AUTH_VERSION = (3, 0) + + def _do_verify_token(self, user_token): + path = '/auth/tokens' + if not self._include_service_catalog: + path += '?nocatalog' + + return self._json_request('GET', + path, + authenticated=True, + headers={'X-Subject-Token': user_token}) + + def _do_fetch_cert_file(self, cert_type): + if cert_type == 'signing': + cert_type = 'certificates' + + return self._adapter.get('/OS-SIMPLE-CERT/%s' % cert_type, + authenticated=False) + + class _TokenCache(object): """Encapsulates the auth_token token cache functionality. @@ -1942,6 +1939,10 @@ def app_factory(global_conf, **local_conf): return AuthProtocol(None, conf) +# NOTE(jamielennox): must be defined after identity server classes +_VERSIONS_TO_ATTEMPT = [_V3IdentityServer, _V2IdentityServer] + + if __name__ == '__main__': def echo_app(environ, start_response): """A WSGI application that echoes the CGI environment to the user.""" diff --git a/keystonemiddleware/tests/test_auth_token_middleware.py b/keystonemiddleware/tests/test_auth_token_middleware.py index 60fbf170..af8db290 100644 --- a/keystonemiddleware/tests/test_auth_token_middleware.py +++ b/keystonemiddleware/tests/test_auth_token_middleware.py @@ -640,6 +640,7 @@ class CommonAuthTokenMiddlewareTest(object): 'auth_port': '1234', 'auth_protocol': 'http', 'auth_uri': None, + 'auth_version': 'v3.0', } self.set_middleware(conf=conf) expected_auth_uri = 'http://[2001:2013:1:f101::1]:1234' @@ -2119,7 +2120,9 @@ class DelayedAuthTests(BaseAuthTokenMiddlewareTest): def test_header_in_401(self): body = uuid.uuid4().hex auth_uri = 'http://local.test' - conf = {'delay_auth_decision': 'True', 'auth_uri': auth_uri} + conf = {'delay_auth_decision': 'True', + 'auth_version': 'v3.0', + 'auth_uri': auth_uri} self.fake_app = new_app('401 Unauthorized', body) self.set_middleware(conf=conf)