Merge "Convert auth_token middleware to use sessions"

This commit is contained in:
Jenkins 2014-08-06 00:54:45 +00:00 committed by Gerrit Code Review
commit 5a46b4874f
2 changed files with 147 additions and 243 deletions

View File

@ -149,14 +149,16 @@ import contextlib
import datetime import datetime
import logging import logging
import os import os
import requests
import stat import stat
import tempfile import tempfile
import time import time
from keystoneclient import access from keystoneclient import access
from keystoneclient.auth.identity import v2
from keystoneclient.auth import token_endpoint
from keystoneclient.common import cms from keystoneclient.common import cms
from keystoneclient import exceptions from keystoneclient import exceptions
from keystoneclient import session
import netaddr import netaddr
from oslo.config import cfg from oslo.config import cfg
import six import six
@ -466,34 +468,47 @@ class AuthProtocol(object):
(True, 'true', 't', '1', 'on', 'yes', 'y') (True, 'true', 't', '1', 'on', 'yes', 'y')
) )
self._identity_uri = self._conf_get('identity_uri')
# NOTE(jamielennox): it does appear here that our default 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.')
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')
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('/')
self._session = self._session_factory()
self._http_request_max_retries = self._conf_get(
'http_request_max_retries')
self._include_service_catalog = self._conf_get( self._include_service_catalog = self._conf_get(
'include_service_catalog') 'include_service_catalog')
http_connect_timeout_cfg = self._conf_get('http_connect_timeout') self._identity_server = self._identity_server_factory()
http_connect_timeout = (http_connect_timeout_cfg and
int(http_connect_timeout_cfg))
http_request_max_retries = self._conf_get('http_request_max_retries')
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 # signing
self._signing_dirname = self._conf_get('signing_dir') self._signing_dirname = self._conf_get('signing_dir')
@ -511,18 +526,9 @@ 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
memcache_security_strategy = ( self._memcache_security_strategy = (
self._conf_get('memcache_security_strategy')) self._conf_get('memcache_security_strategy'))
self._token_cache = self._token_cache_factory()
self._token_cache = _TokenCache(
self._LOG,
cache_time=int(self._conf_get('token_cache_time')),
hash_algorithms=self._conf_get('hash_algorithms'),
env_cache_name=self._conf_get('cache'),
memcached_servers=self._conf_get('memcached_servers'),
memcache_security_strategy=memcache_security_strategy,
memcache_secret_key=self._conf_get('memcache_secret_key'))
self._token_revocation_list_prop = None self._token_revocation_list_prop = None
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(
@ -1002,6 +1008,60 @@ class AuthProtocol(object):
self._signing_ca_file_name, self._signing_ca_file_name,
self._identity_server.fetch_ca_cert()) self._identity_server.fetch_ca_cert())
# NOTE(hrybacki): This and subsequent factory functions are part of a
# cleanup and better organization effort of AuthProtocol.
def _session_factory(self):
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')
))
# FIXME(jamielennox): Yes. This is wrong. We should be determining the
# plugin to use based on a combination of discovery and inputs. Much
# of this can be changed when we get keystoneclient 0.10. For now this
# hardcoded path is EXACTLY the same as the original auth_token did.
auth_url = '%s/v2.0' % self._identity_uri
admin_token = self._conf_get('admin_token')
if 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.")
sess.auth = token_endpoint.Token(auth_url, admin_token)
else:
sess.auth = v2.Password(
auth_url,
username=self._conf_get('admin_user'),
password=self._conf_get('admin_password'),
tenant_name=self._conf_get('admin_tenant_name'))
return sess
def _identity_server_factory(self):
identity_server = _IdentityServer(
self._LOG,
self._session,
include_service_catalog=self._include_service_catalog,
identity_uri=self._identity_uri,
auth_uri=self._conf_get('auth_uri'),
http_request_max_retries=self._http_request_max_retries,
auth_version=self._conf_get('auth_version'))
return identity_server
def _token_cache_factory(self):
token_cache = _TokenCache(
self._LOG,
cache_time=int(self._conf_get('token_cache_time')),
hash_algorithms=self._conf_get('hash_algorithms'),
env_cache_name=self._conf_get('cache'),
memcached_servers=self._conf_get('memcached_servers'),
memcache_security_strategy=self._memcache_security_strategy,
memcache_secret_key=self._conf_get('memcache_secret_key'))
return token_cache
class _CachePool(list): class _CachePool(list):
"""A lazy pool of cache references.""" """A lazy pool of cache references."""
@ -1039,13 +1099,8 @@ class _IdentityServer(object):
operations. operations.
""" """
def __init__(self, log, session, include_service_catalog=None,
def __init__(self, log, include_service_catalog=None, identity_uri=None, identity_uri=None, auth_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): http_request_max_retries=None, auth_version=None):
self._LOG = log self._LOG = log
self._include_service_catalog = include_service_catalog self._include_service_catalog = include_service_catalog
@ -1055,27 +1110,7 @@ class _IdentityServer(object):
self._identity_uri = identity_uri self._identity_uri = identity_uri
self.auth_uri = auth_uri self.auth_uri = auth_uri
# NOTE(jamielennox): it does appear here that our defaults arguments self._session = session
# 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: if self.auth_uri is None:
self._LOG.warning( self._LOG.warning(
@ -1090,28 +1125,6 @@ class _IdentityServer(object):
self.auth_uri = urllib.parse.urljoin(self._identity_uri, '/') self.auth_uri = urllib.parse.urljoin(self._identity_uri, '/')
self.auth_uri = self.auth_uri.rstrip('/') 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._auth_version = None
self._http_request_max_retries = http_request_max_retries self._http_request_max_retries = http_request_max_retries
@ -1127,58 +1140,55 @@ class _IdentityServer(object):
:raise ServiceError: if unable to authenticate token :raise ServiceError: if unable to authenticate token
""" """
user_token = _safe_quote(user_token)
# Determine the highest api version we can use. # Determine the highest api version we can use.
if not self._auth_version: if not self._auth_version:
self._auth_version = self._choose_api_version() self._auth_version = self._choose_api_version()
if self._auth_version == 'v3.0': if self._auth_version == 'v3.0':
headers = {'X-Auth-Token': self.get_admin_token(), headers = {'X-Subject-Token': user_token}
'X-Subject-Token': _safe_quote(user_token)}
path = '/v3/auth/tokens' path = '/v3/auth/tokens'
if not self._include_service_catalog: if not self._include_service_catalog:
# NOTE(gyee): only v3 API support this option # NOTE(gyee): only v3 API support this option
path = path + '?nocatalog' path = path + '?nocatalog'
else:
headers = {}
path = '/v2.0/tokens/%s' % user_token
try:
response, data = self._json_request( response, data = self._json_request(
'GET', 'GET',
path, path,
additional_headers=headers) authenticated=True,
else: headers=headers)
headers = {'X-Auth-Token': self.get_admin_token()} except exceptions.NotFound as e:
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') self._LOG.warn('Authorization failed for token')
raise InvalidUserToken('Token authorization failed') self._LOG.warn('Identity response: %s' % e.response.text)
if response.status_code == 401: except exceptions.Unauthorized as e:
self._LOG.info( self._LOG.info('Keystone rejected authorization')
'Keystone rejected admin token, resetting') self._LOG.warn('Identity response: %s' % e.response.text)
self._admin_token = None
else:
self._LOG.error('Bad response code while validating token: %s',
response.status_code)
if retry: if retry:
self._LOG.info('Retrying validation') self._LOG.info('Retrying validation')
return self.verify_token(user_token, False) return self.verify_token(user_token, False)
except exceptions.HttpError as e:
self._LOG.error('Bad response code while validating token: %s',
e.http_status)
self._LOG.warn('Identity response: %s' % e.response.text)
else: else:
self._LOG.warn('Invalid user token. Keystone response: %s', data) if response.status_code == 200:
return data
raise InvalidUserToken() raise InvalidUserToken()
def fetch_revocation_list(self, retry=True): def fetch_revocation_list(self):
headers = {'X-Auth-Token': self.get_admin_token()} try:
response, data = self._json_request('GET', '/v2.0/tokens/revoked', response, data = self._json_request('GET', '/v2.0/tokens/revoked',
additional_headers=headers) authenticated=True)
if response.status_code == 401: except exceptions.HTTPError as e:
if retry: raise ServiceError('Failed to fetch token revocation list: %d' %
self._LOG.info( e.http_status)
'Keystone rejected admin token, resetting admin token')
self._admin_token = None
return self.fetch_revocation_list(retry=False)
if response.status_code != 200: if response.status_code != 200:
raise ServiceError('Unable to fetch token revocation list.') raise ServiceError('Unable to fetch token revocation list.')
if 'signed' not in data: if 'signed' not in data:
@ -1226,7 +1236,7 @@ class _IdentityServer(object):
def _get_supported_versions(self): def _get_supported_versions(self):
versions = [] versions = []
response, data = self._json_request('GET', '/') response, data = self._json_request('GET', '/', authenticated=False)
if response.status_code == 501: if response.status_code == 501:
self._LOG.warning( self._LOG.warning(
'Old keystone installation found...assuming v2.0') 'Old keystone installation found...assuming v2.0')
@ -1249,27 +1259,6 @@ class _IdentityServer(object):
', '.join(versions)) ', '.join(versions))
return 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): def _http_request(self, method, path, **kwargs):
"""HTTP request helper used to make unspecified content type requests. """HTTP request helper used to make unspecified content type requests.
@ -1281,23 +1270,18 @@ class _IdentityServer(object):
""" """
url = '%s/%s' % (self._identity_uri, path.lstrip('/')) 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 RETRIES = self._http_request_max_retries
retry = 0 retry = 0
while True: while True:
try: try:
response = requests.request(method, url, **kwargs) response = self._session.request(url, method, **kwargs)
break break
except exceptions.HTTPError:
# NOTE(hrybacki): unlike the requests library that return
# response object with a status code e.g. 400, http failures
# in session take these responses and create HTTPError
# exceptions to be handled at a higher level.
raise
except Exception as e: except Exception as e:
if retry >= RETRIES: if retry >= RETRIES:
self._LOG.error('HTTP connection exception: %s', e) self._LOG.error('HTTP connection exception: %s', e)
@ -1309,30 +1293,18 @@ class _IdentityServer(object):
return response return response
def _json_request(self, method, path, body=None, additional_headers=None): def _json_request(self, method, path, **kwargs):
"""HTTP request helper used to make json requests. """HTTP request helper used to make json requests.
:param method: http method :param method: http method
:param path: relative request url :param path: relative request url
:param body: dict to encode to json as request body. Optional. :param **kwargs: additional parameters used by session or endpoint
:param additional_headers: dict of additional headers to send with
http request. Optional.
:return (http response object, response body parsed as json) :return (http response object, response body parsed as json)
:raise ServerError when unable to communicate with keystone :raise ServerError when unable to communicate with keystone
""" """
kwargs = { headers = kwargs.setdefault('headers', {})
'headers': { headers['Accept'] = 'application/json'
'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) response = self._http_request(method, path, **kwargs)
@ -1344,48 +1316,6 @@ class _IdentityServer(object):
return response, 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): def _fetch_cert_file(self, cert_type):
if not self._auth_version: if not self._auth_version:
self._auth_version = self._choose_api_version() self._auth_version = self._choose_api_version()
@ -1396,7 +1326,10 @@ class _IdentityServer(object):
path = '/v3/OS-SIMPLE-CERT/' + cert_type path = '/v3/OS-SIMPLE-CERT/' + cert_type
else: else:
path = '/v2.0/certificates/' + cert_type path = '/v2.0/certificates/' + cert_type
response = self._http_request('GET', path) try:
response = self._http_request('GET', path, authenticated=False)
except exceptions.HTTPError as e:
raise exceptions.CertificateConfigError(e.details)
if response.status_code != 200: if response.status_code != 200:
raise exceptions.CertificateConfigError(response.text) raise exceptions.CertificateConfigError(response.text)
return response.text return response.text

View File

@ -273,35 +273,6 @@ class BaseAuthTokenMiddlewareTest(testtools.TestCase):
httpretty.core.HTTPrettyRequestEmpty) httpretty.core.HTTPrettyRequestEmpty)
class MultiStepAuthTokenMiddlewareTest(BaseAuthTokenMiddlewareTest,
testresources.ResourcedTestCase):
resources = [('examples', client_fixtures.EXAMPLES_RESOURCE)]
@httpretty.activate
def test_fetch_revocation_list_with_expire(self):
self.set_middleware()
# Get a token, then try to retrieve revocation list and get a 401.
# Get a new token, try to retrieve revocation list and return 200.
httpretty.register_uri(httpretty.POST, "%s/v2.0/tokens" % BASE_URI,
body=FAKE_ADMIN_TOKEN)
responses = [httpretty.Response(body='', status=401),
httpretty.Response(
body=self.examples.SIGNED_REVOCATION_LIST)]
httpretty.register_uri(httpretty.GET,
"%s/v2.0/tokens/revoked" % BASE_URI,
responses=responses)
fetched = jsonutils.loads(self.middleware._fetch_revocation_list())
self.assertEqual(fetched, self.examples.REVOCATION_LIST)
# Check that 4 requests have been made
self.assertEqual(len(httpretty.httpretty.latest_requests), 4)
class DiabloAuthTokenMiddlewareTest(BaseAuthTokenMiddlewareTest, class DiabloAuthTokenMiddlewareTest(BaseAuthTokenMiddlewareTest,
testresources.ResourcedTestCase): testresources.ResourcedTestCase):