Merge "Convert auth_token middleware to use sessions"
This commit is contained in:
commit
5a46b4874f
@ -149,14 +149,16 @@ import contextlib
|
||||
import datetime
|
||||
import logging
|
||||
import os
|
||||
import requests
|
||||
import stat
|
||||
import tempfile
|
||||
import time
|
||||
|
||||
from keystoneclient import access
|
||||
from keystoneclient.auth.identity import v2
|
||||
from keystoneclient.auth import token_endpoint
|
||||
from keystoneclient.common import cms
|
||||
from keystoneclient import exceptions
|
||||
from keystoneclient import session
|
||||
import netaddr
|
||||
from oslo.config import cfg
|
||||
import six
|
||||
@ -466,34 +468,47 @@ class AuthProtocol(object):
|
||||
(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(
|
||||
'include_service_catalog')
|
||||
|
||||
http_connect_timeout_cfg = self._conf_get('http_connect_timeout')
|
||||
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'))
|
||||
self._identity_server = self._identity_server_factory()
|
||||
|
||||
# signing
|
||||
self._signing_dirname = self._conf_get('signing_dir')
|
||||
@ -511,18 +526,9 @@ class AuthProtocol(object):
|
||||
val = '%s/revoked.pem' % self._signing_dirname
|
||||
self._revoked_file_name = val
|
||||
|
||||
memcache_security_strategy = (
|
||||
self._memcache_security_strategy = (
|
||||
self._conf_get('memcache_security_strategy'))
|
||||
|
||||
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_cache = self._token_cache_factory()
|
||||
self._token_revocation_list_prop = None
|
||||
self._token_revocation_list_fetched_time_prop = None
|
||||
self._token_revocation_list_cache_timeout = datetime.timedelta(
|
||||
@ -1002,6 +1008,60 @@ class AuthProtocol(object):
|
||||
self._signing_ca_file_name,
|
||||
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):
|
||||
"""A lazy pool of cache references."""
|
||||
@ -1039,13 +1099,8 @@ class _IdentityServer(object):
|
||||
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,
|
||||
def __init__(self, log, session, include_service_catalog=None,
|
||||
identity_uri=None, auth_uri=None,
|
||||
http_request_max_retries=None, auth_version=None):
|
||||
self._LOG = log
|
||||
self._include_service_catalog = include_service_catalog
|
||||
@ -1055,27 +1110,7 @@ class _IdentityServer(object):
|
||||
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('/')
|
||||
self._session = session
|
||||
|
||||
if self.auth_uri is None:
|
||||
self._LOG.warning(
|
||||
@ -1090,28 +1125,6 @@ class _IdentityServer(object):
|
||||
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
|
||||
|
||||
@ -1127,58 +1140,55 @@ class _IdentityServer(object):
|
||||
:raise ServiceError: if unable to authenticate token
|
||||
|
||||
"""
|
||||
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()
|
||||
|
||||
if self._auth_version == 'v3.0':
|
||||
headers = {'X-Auth-Token': self.get_admin_token(),
|
||||
'X-Subject-Token': _safe_quote(user_token)}
|
||||
headers = {'X-Subject-Token': user_token}
|
||||
path = '/v3/auth/tokens'
|
||||
if not self._include_service_catalog:
|
||||
# NOTE(gyee): only v3 API support this option
|
||||
path = path + '?nocatalog'
|
||||
|
||||
else:
|
||||
headers = {}
|
||||
path = '/v2.0/tokens/%s' % user_token
|
||||
|
||||
try:
|
||||
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:
|
||||
authenticated=True,
|
||||
headers=headers)
|
||||
except exceptions.NotFound as e:
|
||||
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.warn('Identity response: %s' % e.response.text)
|
||||
except exceptions.Unauthorized as e:
|
||||
self._LOG.info('Keystone rejected authorization')
|
||||
self._LOG.warn('Identity response: %s' % e.response.text)
|
||||
if retry:
|
||||
self._LOG.info('Retrying validation')
|
||||
return self.verify_token(user_token, False)
|
||||
except exceptions.HttpError as e:
|
||||
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)
|
||||
e.http_status)
|
||||
self._LOG.warn('Identity response: %s' % e.response.text)
|
||||
else:
|
||||
self._LOG.warn('Invalid user token. Keystone response: %s', data)
|
||||
if response.status_code == 200:
|
||||
return 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)
|
||||
def fetch_revocation_list(self):
|
||||
try:
|
||||
response, data = self._json_request('GET', '/v2.0/tokens/revoked',
|
||||
authenticated=True)
|
||||
except exceptions.HTTPError as e:
|
||||
raise ServiceError('Failed to fetch token revocation list: %d' %
|
||||
e.http_status)
|
||||
if response.status_code != 200:
|
||||
raise ServiceError('Unable to fetch token revocation list.')
|
||||
if 'signed' not in data:
|
||||
@ -1226,7 +1236,7 @@ class _IdentityServer(object):
|
||||
|
||||
def _get_supported_versions(self):
|
||||
versions = []
|
||||
response, data = self._json_request('GET', '/')
|
||||
response, data = self._json_request('GET', '/', authenticated=False)
|
||||
if response.status_code == 501:
|
||||
self._LOG.warning(
|
||||
'Old keystone installation found...assuming v2.0')
|
||||
@ -1249,27 +1259,6 @@ class _IdentityServer(object):
|
||||
', '.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.
|
||||
|
||||
@ -1281,23 +1270,18 @@ class _IdentityServer(object):
|
||||
"""
|
||||
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)
|
||||
response = self._session.request(url, method, **kwargs)
|
||||
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:
|
||||
if retry >= RETRIES:
|
||||
self._LOG.error('HTTP connection exception: %s', e)
|
||||
@ -1309,30 +1293,18 @@ class _IdentityServer(object):
|
||||
|
||||
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.
|
||||
|
||||
: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.
|
||||
:param **kwargs: additional parameters used by session or endpoint
|
||||
: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)
|
||||
headers = kwargs.setdefault('headers', {})
|
||||
headers['Accept'] = 'application/json'
|
||||
|
||||
response = self._http_request(method, path, **kwargs)
|
||||
|
||||
@ -1344,48 +1316,6 @@ class _IdentityServer(object):
|
||||
|
||||
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()
|
||||
@ -1396,7 +1326,10 @@ class _IdentityServer(object):
|
||||
path = '/v3/OS-SIMPLE-CERT/' + cert_type
|
||||
else:
|
||||
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:
|
||||
raise exceptions.CertificateConfigError(response.text)
|
||||
return response.text
|
||||
|
@ -273,35 +273,6 @@ class BaseAuthTokenMiddlewareTest(testtools.TestCase):
|
||||
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,
|
||||
testresources.ResourcedTestCase):
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user