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
This commit is contained in:
Brant Knudson 2014-06-24 19:46:53 -05:00
parent ff0ca09e0c
commit cee2f82bc4
2 changed files with 410 additions and 352 deletions

View File

@ -466,55 +466,34 @@ class AuthProtocol(object):
(True, 'true', 't', '1', 'on', 'yes', 'y') (True, 'true', 't', '1', 'on', 'yes', 'y')
) )
# where to find the auth service (we use this to validate tokens) self._include_service_catalog = self._conf_get(
self._identity_uri = self._conf_get('identity_uri') 'include_service_catalog')
self._auth_uri = self._conf_get('auth_uri')
# NOTE(jamielennox): it does appear here that our defaults arguments http_connect_timeout_cfg = self._conf_get('http_connect_timeout')
# are backwards. We need to do it this way so that we can handle the http_connect_timeout = (http_connect_timeout_cfg and
# same deprecation strategy for CONF and the conf variable. int(http_connect_timeout_cfg))
if not self._identity_uri:
self._LOG.warning('Configuring admin URI using auth fragments. '
'This is deprecated, use \'identity_uri\''
' instead.')
auth_host = self._conf_get('auth_host') http_request_max_retries = self._conf_get('http_request_max_retries')
auth_port = int(self._conf_get('auth_port'))
auth_protocol = self._conf_get('auth_protocol')
auth_admin_prefix = self._conf_get('auth_admin_prefix')
if netaddr.valid_ipv6(auth_host): self._identity_server = _IdentityServer(
# Note(dzyu) it is an IPv6 address, so it needs to be wrapped self._LOG, include_service_catalog=self._include_service_catalog,
# with '[]' to generate a valid IPv6 URL, based on identity_uri=self._conf_get('identity_uri'),
# http://www.ietf.org/rfc/rfc2732.txt auth_uri=self._conf_get('auth_uri'),
auth_host = '[%s]' % auth_host auth_host=self._conf_get('auth_host'),
auth_port=int(self._conf_get('auth_port')),
self._identity_uri = '%s://%s:%s' % (auth_protocol, auth_host, auth_protocol=self._conf_get('auth_protocol'),
auth_port) auth_admin_prefix=self._conf_get('auth_admin_prefix'),
if auth_admin_prefix: cert_file=self._conf_get('certfile'),
self._identity_uri = '%s/%s' % (self._identity_uri, key_file=self._conf_get('keyfile'),
auth_admin_prefix.strip('/')) ssl_ca_file=self._conf_get('cafile'),
else: ssl_insecure=self._conf_get('insecure'),
self._identity_uri = self._identity_uri.rstrip('/') admin_token=self._conf_get('admin_token'),
admin_user=self._conf_get('admin_user'),
if self._auth_uri is None: admin_password=self._conf_get('admin_password'),
self._LOG.warning( admin_tenant_name=self._conf_get('admin_tenant_name'),
'Configuring auth_uri to point to the public identity ' http_connect_timeout=http_connect_timeout,
'endpoint is required; clients may not be able to ' http_request_max_retries=http_request_max_retries,
'authenticate against an admin endpoint') auth_version=self._conf_get('auth_version'))
# 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')
# signing # signing
self._signing_dirname = self._conf_get('signing_dir') self._signing_dirname = self._conf_get('signing_dir')
@ -532,20 +511,6 @@ class AuthProtocol(object):
val = '%s/revoked.pem' % self._signing_dirname val = '%s/revoked.pem' % self._signing_dirname
self._revoked_file_name = val 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 = ( memcache_security_strategy = (
self._conf_get('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_fetched_time_prop = None
self._token_revocation_list_cache_timeout = datetime.timedelta( self._token_revocation_list_cache_timeout = datetime.timedelta(
seconds=self._conf_get('revocation_cache_time')) 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( self._check_revocations_for_cached = self._conf_get(
'check_revocations_for_cached') 'check_revocations_for_cached')
@ -582,64 +538,6 @@ class AuthProtocol(object):
else: else:
return CONF.keystone_authtoken[name] 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): def __call__(self, env, start_response):
"""Handle incoming request. """Handle incoming request.
@ -734,149 +632,12 @@ class AuthProtocol(object):
:returns HTTPUnauthorized http response :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)] headers = [('WWW-Authenticate', header_val)]
resp = _MiniResp('Authentication required', env, headers) resp = _MiniResp('Authentication required', env, headers)
start_response('401 Unauthorized', resp.headers) start_response('401 Unauthorized', resp.headers)
return resp.body 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'] = '<SANITIZED>'
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): def _validate_user_token(self, user_token, env, retry=True):
"""Authenticate user token """Authenticate user token
@ -913,7 +674,7 @@ class AuthProtocol(object):
verified = self._verify_signed_token(user_token, token_ids) verified = self._verify_signed_token(user_token, token_ids)
data = jsonutils.loads(verified) data = jsonutils.loads(verified)
else: else:
data = self._verify_uuid_token(user_token, retry) data = self._identity_server.verify_token(user_token, retry)
expires = _confirm_token_not_expired(data) expires = _confirm_token_not_expired(data)
self._confirm_token_bind(data, env) self._confirm_token_bind(data, env)
self._token_cache.store(token_id, data, expires) self._token_cache.store(token_id, data, expires)
@ -1078,59 +839,6 @@ class AuthProtocol(object):
'identifier': identifier}) 'identifier': identifier})
self._invalid_user_token() 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): def _is_signed_token_revoked(self, token_ids):
"""Indicate whether the token appears in the revocation list.""" """Indicate whether the token appears in the revocation list."""
for token_id in token_ids: for token_id in token_ids:
@ -1280,42 +988,19 @@ class AuthProtocol(object):
self._token_revocation_list_fetched_time = timeutils.utcnow() self._token_revocation_list_fetched_time = timeutils.utcnow()
self._atomic_write_to_signing_dir(self._revoked_file_name, value) self._atomic_write_to_signing_dir(self._revoked_file_name, value)
def _fetch_revocation_list(self, retry=True): def _fetch_revocation_list(self):
headers = {'X-Auth-Token': self._get_admin_token()} revocation_list_data = self._identity_server.fetch_revocation_list()
response, data = self._json_request('GET', '/v2.0/tokens/revoked', return self._cms_verify(revocation_list_data)
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_signing_cert(self): 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): 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): class _CachePool(list):
@ -1345,6 +1030,378 @@ class _CachePool(list):
self.append(c) 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'] = '<SANITIZED>'
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): class _TokenCache(object):
"""Encapsulates the auth_token token cache functionality. """Encapsulates the auth_token token cache functionality.

View File

@ -581,7 +581,8 @@ class CommonAuthTokenMiddlewareTest(object):
} }
self.set_middleware(conf=conf) self.set_middleware(conf=conf)
expected_auth_uri = 'http://[2001:2013:1:f101::1]:1234' 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): def assert_valid_request_200(self, token, with_catalog=True):
req = webob.Request.blank('/') req = webob.Request.blank('/')