From cee2f82bc40861b75a629c80ed30625f5ed174da Mon Sep 17 00:00:00 2001 From: Brant Knudson Date: Tue, 24 Jun 2014 19:46:53 -0500 Subject: [PATCH] Refactor auth_token, move identity server members to class This change moves the identity server-related members to their own class from AuthToken. Change-Id: Ia75b5aa04827af59495918085cd4d858c9e66419 --- keystonemiddleware/auth_token.py | 759 ++++++++++-------- .../tests/test_auth_token_middleware.py | 3 +- 2 files changed, 410 insertions(+), 352 deletions(-) diff --git a/keystonemiddleware/auth_token.py b/keystonemiddleware/auth_token.py index 2146f07a..ada415c6 100644 --- a/keystonemiddleware/auth_token.py +++ b/keystonemiddleware/auth_token.py @@ -466,55 +466,34 @@ class AuthProtocol(object): (True, 'true', 't', '1', 'on', 'yes', 'y') ) - # where to find the auth service (we use this to validate tokens) - self._identity_uri = self._conf_get('identity_uri') - self._auth_uri = self._conf_get('auth_uri') + self._include_service_catalog = self._conf_get( + 'include_service_catalog') - # NOTE(jamielennox): it does appear here that our defaults arguments - # are backwards. We need to do it this way so that we can handle the - # same deprecation strategy for CONF and the conf variable. - if not self._identity_uri: - self._LOG.warning('Configuring admin URI using auth fragments. ' - 'This is deprecated, use \'identity_uri\'' - ' instead.') + http_connect_timeout_cfg = self._conf_get('http_connect_timeout') + http_connect_timeout = (http_connect_timeout_cfg and + int(http_connect_timeout_cfg)) - 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') + http_request_max_retries = self._conf_get('http_request_max_retries') - if netaddr.valid_ipv6(auth_host): - # Note(dzyu) it is an IPv6 address, so it needs to be wrapped - # with '[]' to generate a valid IPv6 URL, based on - # http://www.ietf.org/rfc/rfc2732.txt - auth_host = '[%s]' % auth_host - - self._identity_uri = '%s://%s:%s' % (auth_protocol, auth_host, - auth_port) - if auth_admin_prefix: - self._identity_uri = '%s/%s' % (self._identity_uri, - auth_admin_prefix.strip('/')) - else: - self._identity_uri = self._identity_uri.rstrip('/') - - if self._auth_uri is None: - self._LOG.warning( - 'Configuring auth_uri to point to the public identity ' - 'endpoint is required; clients may not be able to ' - 'authenticate against an admin endpoint') - - # FIXME(dolph): drop support for this fallback behavior as - # documented in bug 1207517. - # NOTE(jamielennox): we urljoin '/' to get just the base URI as - # this is the original behaviour. - self._auth_uri = urllib.parse.urljoin(self._identity_uri, '/') - self._auth_uri = self._auth_uri.rstrip('/') - - # SSL - self._cert_file = self._conf_get('certfile') - self._key_file = self._conf_get('keyfile') - self._ssl_ca_file = self._conf_get('cafile') - self._ssl_insecure = self._conf_get('insecure') + self._identity_server = _IdentityServer( + self._LOG, include_service_catalog=self._include_service_catalog, + identity_uri=self._conf_get('identity_uri'), + auth_uri=self._conf_get('auth_uri'), + 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'), + cert_file=self._conf_get('certfile'), + key_file=self._conf_get('keyfile'), + ssl_ca_file=self._conf_get('cafile'), + ssl_insecure=self._conf_get('insecure'), + admin_token=self._conf_get('admin_token'), + admin_user=self._conf_get('admin_user'), + admin_password=self._conf_get('admin_password'), + admin_tenant_name=self._conf_get('admin_tenant_name'), + http_connect_timeout=http_connect_timeout, + http_request_max_retries=http_request_max_retries, + auth_version=self._conf_get('auth_version')) # signing self._signing_dirname = self._conf_get('signing_dir') @@ -532,20 +511,6 @@ class AuthProtocol(object): val = '%s/revoked.pem' % self._signing_dirname self._revoked_file_name = val - # Credentials used to verify this component with the Auth service since - # validating tokens is a privileged call - self._admin_token = self._conf_get('admin_token') - if self._admin_token: - self._LOG.warning( - "The admin_token option in the auth_token middleware is " - "deprecated and should not be used. The admin_user and " - "admin_password options should be used instead. The " - "admin_token option may be removed in a future release.") - self._admin_token_expiry = None - self._admin_user = self._conf_get('admin_user') - self._admin_password = self._conf_get('admin_password') - self._admin_tenant_name = self._conf_get('admin_tenant_name') - memcache_security_strategy = ( self._conf_get('memcache_security_strategy')) @@ -562,15 +527,6 @@ class AuthProtocol(object): self._token_revocation_list_fetched_time_prop = None self._token_revocation_list_cache_timeout = datetime.timedelta( seconds=self._conf_get('revocation_cache_time')) - http_connect_timeout_cfg = self._conf_get('http_connect_timeout') - self._http_connect_timeout = (http_connect_timeout_cfg and - int(http_connect_timeout_cfg)) - self._auth_version = None - self._http_request_max_retries = ( - self._conf_get('http_request_max_retries')) - - self._include_service_catalog = self._conf_get( - 'include_service_catalog') self._check_revocations_for_cached = self._conf_get( 'check_revocations_for_cached') @@ -582,64 +538,6 @@ class AuthProtocol(object): else: return CONF.keystone_authtoken[name] - 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._conf_get('auth_version'): - version_to_use = self._conf_get('auth_version') - self._LOG.info('Auth Token proceeding with requested %s apis', - version_to_use) - else: - version_to_use = None - versions_supported_by_server = self._get_supported_versions() - if versions_supported_by_server: - for version in _LIST_OF_VERSIONS_TO_ATTEMPT: - if version in versions_supported_by_server: - version_to_use = version - break - if version_to_use: - self._LOG.info('Auth Token confirmed use of %s apis', - version_to_use) - else: - self._LOG.error( - 'Attempted versions [%s] not in list supported by ' - 'server [%s]', - ', '.join(_LIST_OF_VERSIONS_TO_ATTEMPT), - ', '.join(versions_supported_by_server)) - raise ServiceError('No compatible apis supported by server') - return version_to_use - - def _get_supported_versions(self): - versions = [] - response, data = self._json_request('GET', '/') - if response.status_code == 501: - msg = 'Old keystone installation found...assuming v2.0' - self._LOG.warning(msg) - versions.append('v2.0') - elif response.status_code != 300: - self._LOG.error('Unable to get version info from keystone: %s', - response.status_code) - raise ServiceError('Unable to get version info from keystone') - else: - try: - for version in data['versions']['values']: - versions.append(version['id']) - except KeyError: - self._LOG.error( - 'Invalid version response format from server') - raise ServiceError('Unable to parse version response ' - 'from keystone') - - self._LOG.debug('Server reports support for api versions: %s', - ', '.join(versions)) - return versions - def __call__(self, env, start_response): """Handle incoming request. @@ -734,149 +632,12 @@ class AuthProtocol(object): :returns HTTPUnauthorized http response """ - header_val = 'Keystone uri=\'%s\'' % self._auth_uri + header_val = 'Keystone uri=\'%s\'' % self._identity_server.auth_uri headers = [('WWW-Authenticate', header_val)] resp = _MiniResp('Authentication required', env, headers) start_response('401 Unauthorized', resp.headers) return resp.body - def _get_admin_token(self): - """Return admin token, possibly fetching a new one. - - if self.admin_token_expiry is set from fetching an admin token, check - it for expiration, and request a new token is the existing token - is about to expire. - - :return admin token id - :raise ServiceError when unable to retrieve token from keystone - - """ - if self._admin_token_expiry: - if _will_expire_soon(self._admin_token_expiry): - self._admin_token = None - - if not self._admin_token: - (self._admin_token, - self._admin_token_expiry) = self._request_admin_token() - - return self._admin_token - - def _http_request(self, method, path, **kwargs): - """HTTP request helper used to make unspecified content type requests. - - :param method: http method - :param path: relative request url - :return (http response object, response body) - :raise ServerError when unable to communicate with keystone - - """ - url = '%s/%s' % (self._identity_uri, path.lstrip('/')) - - kwargs.setdefault('timeout', self._http_connect_timeout) - if self._cert_file and self._key_file: - kwargs['cert'] = (self._cert_file, self._key_file) - elif self._cert_file or self._key_file: - self._LOG.warn('Cannot use only a cert or key file. ' - 'Please provide both. Ignoring.') - - kwargs['verify'] = self._ssl_ca_file or True - if self._ssl_insecure: - kwargs['verify'] = False - - RETRIES = self._http_request_max_retries - retry = 0 - while True: - try: - response = requests.request(method, url, **kwargs) - break - except Exception as e: - if retry >= RETRIES: - self._LOG.error('HTTP connection exception: %s', e) - raise NetworkError('Unable to communicate with keystone') - # NOTE(vish): sleep 0.5, 1, 2 - self._LOG.warn('Retrying on HTTP connection exception: %s', e) - time.sleep(2.0 ** retry / 2) - retry += 1 - - return response - - def _json_request(self, method, path, body=None, additional_headers=None): - """HTTP request helper used to make json requests. - - :param method: http method - :param path: relative request url - :param body: dict to encode to json as request body. Optional. - :param additional_headers: dict of additional headers to send with - http request. Optional. - :return (http response object, response body parsed as json) - :raise ServerError when unable to communicate with keystone - - """ - kwargs = { - 'headers': { - 'Content-type': 'application/json', - 'Accept': 'application/json', - }, - } - - if additional_headers: - kwargs['headers'].update(additional_headers) - - if body: - kwargs['data'] = jsonutils.dumps(body) - - response = self._http_request(method, path, **kwargs) - - try: - data = jsonutils.loads(response.text) - except ValueError: - self._LOG.debug('Keystone did not return json-encoded body') - data = {} - - return response, data - - def _request_admin_token(self): - """Retrieve new token as admin user from keystone. - - :return token id upon success - :raises ServerError when unable to communicate with keystone - - Irrespective of the auth version we are going to use for the - user token, for simplicity we always use a v2 admin token to - validate the user token. - - """ - params = { - 'auth': { - 'passwordCredentials': { - 'username': self._admin_user, - 'password': self._admin_password, - }, - 'tenantName': self._admin_tenant_name, - } - } - - response, data = self._json_request('POST', - '/v2.0/tokens', - body=params) - - try: - token = data['access']['token']['id'] - expiry = data['access']['token']['expires'] - if not (token and expiry): - raise AssertionError('invalid token or expire') - datetime_expiry = timeutils.parse_isotime(expiry) - return (token, timeutils.normalize_time(datetime_expiry)) - except (AssertionError, KeyError): - self._LOG.warn( - 'Unexpected response from keystone service: %s', data) - raise ServiceError('invalid json response') - except (ValueError): - data['access']['token']['id'] = '' - self._LOG.warn( - 'Unable to parse expiration time from token: %s', data) - raise ServiceError('invalid json response') - def _validate_user_token(self, user_token, env, retry=True): """Authenticate user token @@ -913,7 +674,7 @@ class AuthProtocol(object): verified = self._verify_signed_token(user_token, token_ids) data = jsonutils.loads(verified) else: - data = self._verify_uuid_token(user_token, retry) + data = self._identity_server.verify_token(user_token, retry) expires = _confirm_token_not_expired(data) self._confirm_token_bind(data, env) self._token_cache.store(token_id, data, expires) @@ -1078,59 +839,6 @@ class AuthProtocol(object): 'identifier': identifier}) self._invalid_user_token() - def _verify_uuid_token(self, user_token, retry=True): - """Authenticate user token with keystone. - - :param user_token: user's token id - :param retry: flag that forces the middleware to retry - user authentication when an indeterminate - response is received. Optional. - :return: token object received from keystone on success - :raise InvalidUserToken: if token is rejected - :raise ServiceError: if unable to authenticate token - - """ - # Determine the highest api version we can use. - if not self._auth_version: - self._auth_version = self._choose_api_version() - - if self._auth_version == 'v3.0': - headers = {'X-Auth-Token': self._get_admin_token(), - 'X-Subject-Token': _safe_quote(user_token)} - path = '/v3/auth/tokens' - if not self._include_service_catalog: - # NOTE(gyee): only v3 API support this option - path = path + '?nocatalog' - response, data = self._json_request( - 'GET', - path, - additional_headers=headers) - else: - headers = {'X-Auth-Token': self._get_admin_token()} - response, data = self._json_request( - 'GET', - '/v2.0/tokens/%s' % _safe_quote(user_token), - additional_headers=headers) - - if response.status_code == 200: - return data - if response.status_code == 404: - self._LOG.warn('Authorization failed for token') - raise InvalidUserToken('Token authorization failed') - if response.status_code == 401: - self._LOG.info('Keystone rejected admin token, resetting') - self.admin_token = None - else: - self._LOG.error('Bad response code while validating token: %s', - response.status_code) - if retry: - self._LOG.info('Retrying validation') - return self._verify_uuid_token(user_token, False) - else: - self._LOG.warn('Invalid user token. Keystone response: %s', data) - - raise InvalidUserToken() - def _is_signed_token_revoked(self, token_ids): """Indicate whether the token appears in the revocation list.""" for token_id in token_ids: @@ -1280,42 +988,19 @@ class AuthProtocol(object): self._token_revocation_list_fetched_time = timeutils.utcnow() self._atomic_write_to_signing_dir(self._revoked_file_name, value) - def _fetch_revocation_list(self, retry=True): - headers = {'X-Auth-Token': self._get_admin_token()} - response, data = self._json_request('GET', '/v2.0/tokens/revoked', - additional_headers=headers) - if response.status_code == 401: - if retry: - self._LOG.info( - 'Keystone rejected admin token, resetting admin token') - self._admin_token = None - return self._fetch_revocation_list(retry=False) - if response.status_code != 200: - raise ServiceError('Unable to fetch token revocation list.') - if 'signed' not in data: - raise ServiceError('Revocation list improperly formatted.') - return self._cms_verify(data['signed']) - - def _fetch_cert_file(self, cert_file_name, cert_type): - if not self._auth_version: - self._auth_version = self._choose_api_version() - - if self._auth_version == 'v3.0': - if cert_type == 'signing': - cert_type = 'certificates' - path = '/v3/OS-SIMPLE-CERT/' + cert_type - else: - path = '/v2.0/certificates/' + cert_type - response = self._http_request('GET', path) - if response.status_code != 200: - raise exceptions.CertificateConfigError(response.text) - self._atomic_write_to_signing_dir(cert_file_name, response.text) + def _fetch_revocation_list(self): + revocation_list_data = self._identity_server.fetch_revocation_list() + return self._cms_verify(revocation_list_data) def _fetch_signing_cert(self): - self._fetch_cert_file(self._signing_cert_file_name, 'signing') + self._atomic_write_to_signing_dir( + self._signing_cert_file_name, + self._identity_server.fetch_signing_cert()) def _fetch_ca_cert(self): - self._fetch_cert_file(self._signing_ca_file_name, 'ca') + self._atomic_write_to_signing_dir( + self._signing_ca_file_name, + self._identity_server.fetch_ca_cert()) class _CachePool(list): @@ -1345,6 +1030,378 @@ class _CachePool(list): self.append(c) +class _IdentityServer(object): + """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, + etc. This class encapsulates the data and methods to perform these + operations. + + """ + + def __init__(self, log, include_service_catalog=None, identity_uri=None, + auth_uri=None, auth_host=None, auth_port=None, + auth_protocol=None, auth_admin_prefix=None, cert_file=None, + key_file=None, ssl_ca_file=None, ssl_insecure=None, + admin_token=None, admin_user=None, admin_password=None, + admin_tenant_name=None, http_connect_timeout=None, + http_request_max_retries=None, auth_version=None): + self._LOG = log + self._include_service_catalog = include_service_catalog + self._req_auth_version = auth_version + + # where to find the auth service (we use this to validate tokens) + self._identity_uri = identity_uri + self.auth_uri = auth_uri + + # NOTE(jamielennox): it does appear here that our defaults arguments + # are backwards. We need to do it this way so that we can handle the + # same deprecation strategy for CONF and the conf variable. + if not self._identity_uri: + self._LOG.warning('Configuring admin URI using auth fragments. ' + 'This is deprecated, use \'identity_uri\'' + ' instead.') + + if netaddr.valid_ipv6(auth_host): + # Note(dzyu) it is an IPv6 address, so it needs to be wrapped + # with '[]' to generate a valid IPv6 URL, based on + # http://www.ietf.org/rfc/rfc2732.txt + auth_host = '[%s]' % auth_host + + self._identity_uri = '%s://%s:%s' % (auth_protocol, auth_host, + auth_port) + if auth_admin_prefix: + self._identity_uri = '%s/%s' % (self._identity_uri, + auth_admin_prefix.strip('/')) + else: + self._identity_uri = self._identity_uri.rstrip('/') + + if self.auth_uri is None: + self._LOG.warning( + 'Configuring auth_uri to point to the public identity ' + 'endpoint is required; clients may not be able to ' + 'authenticate against an admin endpoint') + + # FIXME(dolph): drop support for this fallback behavior as + # documented in bug 1207517. + # NOTE(jamielennox): we urljoin '/' to get just the base URI as + # this is the original behaviour. + self.auth_uri = urllib.parse.urljoin(self._identity_uri, '/') + self.auth_uri = self.auth_uri.rstrip('/') + + # SSL + self._cert_file = cert_file + self._key_file = key_file + self._ssl_ca_file = ssl_ca_file + self._ssl_insecure = ssl_insecure + + # Credentials used to verify this component with the Auth service since + # validating tokens is a privileged call + self._admin_token = admin_token + if self._admin_token: + self._LOG.warning( + "The admin_token option in the auth_token middleware is " + "deprecated and should not be used. The admin_user and " + "admin_password options should be used instead. The " + "admin_token option may be removed in a future release.") + self._admin_token_expiry = None + self._admin_user = admin_user + self._admin_password = admin_password + self._admin_tenant_name = admin_tenant_name + + self._http_connect_timeout = http_connect_timeout + + self._auth_version = None + self._http_request_max_retries = http_request_max_retries + + def verify_token(self, user_token, retry=True): + """Authenticate user token with keystone. + + :param user_token: user's token id + :param retry: flag that forces the middleware to retry + user authentication when an indeterminate + response is received. Optional. + :return: token object received from keystone on success + :raise InvalidUserToken: if token is rejected + :raise ServiceError: if unable to authenticate token + + """ + # Determine the highest api version we can use. + if not self._auth_version: + self._auth_version = self._choose_api_version() + + if self._auth_version == 'v3.0': + headers = {'X-Auth-Token': self.get_admin_token(), + 'X-Subject-Token': _safe_quote(user_token)} + path = '/v3/auth/tokens' + if not self._include_service_catalog: + # NOTE(gyee): only v3 API support this option + path = path + '?nocatalog' + response, data = self._json_request( + 'GET', + path, + additional_headers=headers) + else: + headers = {'X-Auth-Token': self.get_admin_token()} + response, data = self._json_request( + 'GET', + '/v2.0/tokens/%s' % _safe_quote(user_token), + additional_headers=headers) + + if response.status_code == 200: + return data + if response.status_code == 404: + self._LOG.warn('Authorization failed for token') + raise InvalidUserToken('Token authorization failed') + if response.status_code == 401: + self._LOG.info( + 'Keystone rejected admin token, resetting') + self._admin_token = None + else: + self._LOG.error('Bad response code while validating token: %s', + response.status_code) + if retry: + self._LOG.info('Retrying validation') + return self.verify_token(user_token, False) + else: + self._LOG.warn('Invalid user token. Keystone response: %s', data) + + raise InvalidUserToken() + + def fetch_revocation_list(self, retry=True): + headers = {'X-Auth-Token': self.get_admin_token()} + response, data = self._json_request('GET', '/v2.0/tokens/revoked', + additional_headers=headers) + if response.status_code == 401: + if retry: + self._LOG.info( + 'Keystone rejected admin token, resetting admin token') + self._admin_token = None + return self.fetch_revocation_list(retry=False) + if response.status_code != 200: + raise ServiceError('Unable to fetch token revocation list.') + if 'signed' not in data: + raise ServiceError('Revocation list improperly formatted.') + return data['signed'] + + def fetch_signing_cert(self): + return self._fetch_cert_file('signing') + + 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: + version_to_use = self._req_auth_version + self._LOG.info('Auth Token proceeding with requested %s apis', + version_to_use) + else: + version_to_use = None + versions_supported_by_server = self._get_supported_versions() + if versions_supported_by_server: + for version in _LIST_OF_VERSIONS_TO_ATTEMPT: + if version in versions_supported_by_server: + version_to_use = version + break + if version_to_use: + self._LOG.info('Auth Token confirmed use of %s apis', + version_to_use) + else: + self._LOG.error( + 'Attempted versions [%s] not in list supported by ' + 'server [%s]', + ', '.join(_LIST_OF_VERSIONS_TO_ATTEMPT), + ', '.join(versions_supported_by_server)) + raise ServiceError('No compatible apis supported by server') + return version_to_use + + def _get_supported_versions(self): + versions = [] + response, data = self._json_request('GET', '/') + if response.status_code == 501: + self._LOG.warning( + 'Old keystone installation found...assuming v2.0') + versions.append('v2.0') + elif response.status_code != 300: + self._LOG.error('Unable to get version info from keystone: %s', + response.status_code) + raise ServiceError('Unable to get version info from keystone') + else: + try: + for version in data['versions']['values']: + versions.append(version['id']) + except KeyError: + self._LOG.error( + 'Invalid version response format from server') + raise ServiceError('Unable to parse version response ' + 'from keystone') + + self._LOG.debug('Server reports support for api versions: %s', + ', '.join(versions)) + return versions + + def get_admin_token(self): + """Return admin token, possibly fetching a new one. + + if self._admin_token_expiry is set from fetching an admin token, check + it for expiration, and request a new token is the existing token + is about to expire. + + :return admin token id + :raise ServiceError when unable to retrieve token from keystone + + """ + if self._admin_token_expiry: + if _will_expire_soon(self._admin_token_expiry): + self._admin_token = None + + if not self._admin_token: + (self._admin_token, + self._admin_token_expiry) = self._request_admin_token() + + return self._admin_token + + def _http_request(self, method, path, **kwargs): + """HTTP request helper used to make unspecified content type requests. + + :param method: http method + :param path: relative request url + :return (http response object, response body) + :raise ServerError when unable to communicate with keystone + + """ + url = '%s/%s' % (self._identity_uri, path.lstrip('/')) + + kwargs.setdefault('timeout', self._http_connect_timeout) + if self._cert_file and self._key_file: + kwargs['cert'] = (self._cert_file, self._key_file) + elif self._cert_file or self._key_file: + self._LOG.warn('Cannot use only a cert or key file. ' + 'Please provide both. Ignoring.') + + kwargs['verify'] = self._ssl_ca_file or True + if self._ssl_insecure: + kwargs['verify'] = False + + RETRIES = self._http_request_max_retries + retry = 0 + while True: + try: + response = requests.request(method, url, **kwargs) + break + except Exception as e: + if retry >= RETRIES: + self._LOG.error('HTTP connection exception: %s', e) + raise NetworkError('Unable to communicate with keystone') + # NOTE(vish): sleep 0.5, 1, 2 + self._LOG.warn('Retrying on HTTP connection exception: %s', e) + time.sleep(2.0 ** retry / 2) + retry += 1 + + return response + + def _json_request(self, method, path, body=None, additional_headers=None): + """HTTP request helper used to make json requests. + + :param method: http method + :param path: relative request url + :param body: dict to encode to json as request body. Optional. + :param additional_headers: dict of additional headers to send with + http request. Optional. + :return (http response object, response body parsed as json) + :raise ServerError when unable to communicate with keystone + + """ + kwargs = { + 'headers': { + 'Content-type': 'application/json', + 'Accept': 'application/json', + }, + } + + if additional_headers: + kwargs['headers'].update(additional_headers) + + if body: + kwargs['data'] = jsonutils.dumps(body) + + response = self._http_request(method, path, **kwargs) + + try: + data = jsonutils.loads(response.text) + except ValueError: + self._LOG.debug('Keystone did not return json-encoded body') + data = {} + + return response, data + + def _request_admin_token(self): + """Retrieve new token as admin user from keystone. + + :return token id upon success + :raises ServerError when unable to communicate with keystone + + Irrespective of the auth version we are going to use for the + user token, for simplicity we always use a v2 admin token to + validate the user token. + + """ + params = { + 'auth': { + 'passwordCredentials': { + 'username': self._admin_user, + 'password': self._admin_password, + }, + 'tenantName': self._admin_tenant_name, + } + } + + response, data = self._json_request('POST', + '/v2.0/tokens', + body=params) + + try: + token = data['access']['token']['id'] + expiry = data['access']['token']['expires'] + if not (token and expiry): + raise AssertionError('invalid token or expire') + datetime_expiry = timeutils.parse_isotime(expiry) + return (token, timeutils.normalize_time(datetime_expiry)) + except (AssertionError, KeyError): + self._LOG.warn( + 'Unexpected response from keystone service: %s', data) + raise ServiceError('invalid json response') + except (ValueError): + data['access']['token']['id'] = '' + self._LOG.warn( + 'Unable to parse expiration time from token: %s', data) + raise ServiceError('invalid json response') + + def _fetch_cert_file(self, cert_type): + if not self._auth_version: + self._auth_version = self._choose_api_version() + + if self._auth_version == 'v3.0': + if cert_type == 'signing': + cert_type = 'certificates' + path = '/v3/OS-SIMPLE-CERT/' + cert_type + else: + path = '/v2.0/certificates/' + cert_type + response = self._http_request('GET', path) + if response.status_code != 200: + raise exceptions.CertificateConfigError(response.text) + return response.text + + class _TokenCache(object): """Encapsulates the auth_token token cache functionality. diff --git a/keystonemiddleware/tests/test_auth_token_middleware.py b/keystonemiddleware/tests/test_auth_token_middleware.py index 49a683bf..66fd07ea 100644 --- a/keystonemiddleware/tests/test_auth_token_middleware.py +++ b/keystonemiddleware/tests/test_auth_token_middleware.py @@ -581,7 +581,8 @@ class CommonAuthTokenMiddlewareTest(object): } self.set_middleware(conf=conf) expected_auth_uri = 'http://[2001:2013:1:f101::1]:1234' - self.assertEqual(expected_auth_uri, self.middleware._auth_uri) + self.assertEqual(expected_auth_uri, + self.middleware._identity_server.auth_uri) def assert_valid_request_200(self, token, with_catalog=True): req = webob.Request.blank('/')