Split identity server into v2 and v3

We make the IdentityServer object an abstraction layer between v2 and v3
APIs as the whole object. By doing this we don't have to check at the
beginning of every function whether to use v2 or v3. This will be
important when using auth plugins because the expected workflow is just
to issue the calls and see which ones fail with EndpointNotFound and so
we would have to reorder the code to possibly issue each call twice.

We need the additional test discovery entries as discovery now happens
when you instantiate the IdentityServer object - not when you use it.
This will have no practical difference, it's just that in testing we
sometimes create the object to read values from it.

Change-Id: Ie05d99946c2b1127992cd9a083a2068a6e80398b
This commit is contained in:
Jamie Lennox 2014-10-22 16:14:29 +02:00
parent 563f82727c
commit b4cb4a17e9
2 changed files with 122 additions and 118 deletions

View File

@ -353,7 +353,6 @@ _OPTS = [
_AUTHTOKEN_GROUP = 'keystone_authtoken'
CONF = cfg.CONF
CONF.register_opts(_OPTS, group=_AUTHTOKEN_GROUP)
_LIST_OF_VERSIONS_TO_ATTEMPT = ['v3.0', 'v2.0']
_HEADER_TEMPLATE = {
'X%s-Domain-Id': 'domain_id',
@ -611,12 +610,9 @@ class _AuthTokenPlugin(auth.BaseAuthPlugin):
# NOTE(jamielennox): for backwards compatibility here we don't
# actually use the URL from discovery we hack it up instead. :(
# NOTE(jamielennox): matching the string version here matches the value
# from _LIST_OF_VERSIONS_TO_ATTEMPT. We can use the tuple only match
# in the future.
if version == 'v2.0' or version[0] == 2:
if version[0] == 2:
return '%s/v2.0' % self._identity_uri
elif version == 'v3.0' or version[0] == 3:
elif version[0] == 3:
return '%s/v3' % self._identity_uri
# NOTE(jamielennox): This plugin will only get called from auth_token
@ -732,7 +728,7 @@ class AuthProtocol(object):
self._include_service_catalog = self._conf_get(
'include_service_catalog')
self._identity_server = self._identity_server_factory()
self._identity_server_obj = None
# signing
self._signing_dirname = self._conf_get('signing_dir')
@ -1321,48 +1317,77 @@ class AuthProtocol(object):
self._signing_ca_file_name,
self._identity_server.fetch_ca_cert())
def _identity_server_factory(self):
# NOTE(jamielennox): Loading Session here should be exactly the
# same as calling Session.load_from_conf_options(CONF, GROUP)
# however we can't do that because we have to use _conf_get to support
# the paste.ini options.
sess = session.Session.construct(dict(
cert=self._conf_get('certfile'),
key=self._conf_get('keyfile'),
cacert=self._conf_get('cafile'),
insecure=self._conf_get('insecure'),
timeout=self._conf_get('http_connect_timeout')
))
@property
def _identity_server(self):
if not self._identity_server_obj:
# NOTE(jamielennox): Loading Session here should be exactly the
# same as calling Session.load_from_conf_options(CONF, GROUP)
# however we can't do that because we have to use _conf_get to
# support the paste.ini options.
sess = session.Session.construct(dict(
cert=self._conf_get('certfile'),
key=self._conf_get('keyfile'),
cacert=self._conf_get('cafile'),
insecure=self._conf_get('insecure'),
timeout=self._conf_get('http_connect_timeout')
))
# NOTE(jamielennox): Loading AuthTokenPlugin here should be exactly the
# same as calling _AuthTokenPlugin.load_from_conf_options(CONF, GROUP)
# however we can't do that because we have to use _conf_get to support
# the paste.ini options.
auth_plugin = _AuthTokenPlugin.load_from_options(
auth_host=self._conf_get('auth_host'),
auth_port=int(self._conf_get('auth_port')),
auth_protocol=self._conf_get('auth_protocol'),
auth_admin_prefix=self._conf_get('auth_admin_prefix'),
admin_user=self._conf_get('admin_user'),
admin_password=self._conf_get('admin_password'),
admin_tenant_name=self._conf_get('admin_tenant_name'),
admin_token=self._conf_get('admin_token'),
identity_uri=self._conf_get('identity_uri'),
log=self._LOG)
# NOTE(jamielennox): Loading AuthTokenPlugin here should be exactly
# the same as calling _AuthTokenPlugin.load_from_conf_options(CONF,
# GROUP) however we can't do that because we have to use _conf_get
# to support the paste.ini options.
auth_plugin = _AuthTokenPlugin.load_from_options(
auth_host=self._conf_get('auth_host'),
auth_port=int(self._conf_get('auth_port')),
auth_protocol=self._conf_get('auth_protocol'),
auth_admin_prefix=self._conf_get('auth_admin_prefix'),
admin_user=self._conf_get('admin_user'),
admin_password=self._conf_get('admin_password'),
admin_tenant_name=self._conf_get('admin_tenant_name'),
admin_token=self._conf_get('admin_token'),
identity_uri=self._conf_get('identity_uri'),
log=self._LOG)
adap = adapter.Adapter(
sess,
auth=auth_plugin,
service_type='identity',
interface='admin',
connect_retries=self._conf_get('http_request_max_retries'))
adap = adapter.Adapter(
sess,
auth=auth_plugin,
service_type='identity',
interface='admin',
connect_retries=self._conf_get('http_request_max_retries'))
return _IdentityServer(
self._LOG,
adap,
include_service_catalog=self._include_service_catalog,
auth_uri=self._conf_get('auth_uri'),
auth_version=self._conf_get('auth_version'))
server_class = self._get_server_class(adap)
adap.version = server_class.AUTH_VERSION
self._identity_server_obj = server_class(
self._LOG,
adap,
include_service_catalog=self._include_service_catalog,
auth_uri=self._conf_get('auth_uri'))
return self._identity_server_obj
def _get_server_class(self, adap):
# FIXME(jamielennox): Checking string equality is bad, but consistent
# with the existing code. Fix this to better handle selecting v3.
auth_version = self._conf_get('auth_version')
if auth_version == 'v3.0':
return _V3IdentityServer
elif auth_version:
return _V2IdentityServer
for klass in _VERSIONS_TO_ATTEMPT:
if adap.get_endpoint(version=klass.AUTH_VERSION):
msg = _LI('Auth Token confirmed use of %s apis')
self._LOG.info(msg, auth_version)
return klass
versions = ['v%d.%d' % s.AUTH_VERSION for s in _VERSIONS_TO_ATTEMPT]
self._LOG.error(_LE('No attempted versions [%s] supported by server') %
', '.join(versions))
msg = _('No compatible apis supported by server')
raise ServiceError(msg)
def _token_cache_factory(self):
security_strategy = self._conf_get('memcache_security_strategy')
@ -1449,7 +1474,7 @@ class _MemcacheClientPool(object):
class _IdentityServer(object):
"""Operations on the Identity API server.
"""Base class for operations on the Identity API server.
The auth_token middleware needs to communicate with the Identity API server
to validate UUID tokens, fetch the revocation list, signing certificates,
@ -1457,12 +1482,13 @@ class _IdentityServer(object):
operations.
"""
def __init__(self, log, adap, include_service_catalog=None,
auth_uri=None, auth_version=None):
AUTH_VERSION = None
def __init__(self, log, adap, include_service_catalog=None, auth_uri=None):
self._LOG = log
self._adapter = adap
self._include_service_catalog = include_service_catalog
self._req_auth_version = auth_version
if auth_uri is None:
self._LOG.warning(
@ -1481,7 +1507,6 @@ class _IdentityServer(object):
auth_uri = urllib.parse.urljoin(auth_uri, '/').rstrip('/')
self.auth_uri = auth_uri
self._auth_version = None
def verify_token(self, user_token, retry=True):
"""Authenticate user token with identity server.
@ -1497,31 +1522,8 @@ class _IdentityServer(object):
"""
user_token = _safe_quote(user_token)
# Determine the highest api version we can use.
if not self._auth_version:
self._auth_version = self._choose_api_version()
headers = {}
if self._auth_version == 'v3.0':
headers['X-Subject-Token'] = user_token
version = (3, 0)
path = '/auth/tokens'
if not self._include_service_catalog:
# NOTE(gyee): only v3 API support this option
path = path + '?nocatalog'
else:
version = (2, 0)
path = '/tokens/%s' % user_token
try:
response, data = self._json_request(
'GET',
path,
authenticated=True,
endpoint_filter={'version': version},
headers=headers)
response, data = self._do_verify_token(user_token)
except exceptions.NotFound as e:
self._LOG.warn(_LW('Authorization failed for token'))
self._LOG.warn(_LW('Identity response: %s') % e.response.text)
@ -1563,31 +1565,6 @@ class _IdentityServer(object):
def fetch_ca_cert(self):
return self._fetch_cert_file('ca')
def _choose_api_version(self):
"""Determine the api version that we should use."""
# If the configuration specifies an auth_version we will just
# assume that is correct and use it. We could, of course, check
# that this version is supported by the server, but in case
# there are some problems in the field, we want as little code
# as possible in the way of letting auth_token talk to the
# server.
if self._req_auth_version:
self._LOG.info(_LI('Auth Token proceeding with requested %s apis'),
self._req_auth_version)
return self._req_auth_version
for auth_version in _LIST_OF_VERSIONS_TO_ATTEMPT:
if self._adapter.get_endpoint(version=auth_version):
self._LOG.info(_LI('Auth Token confirmed use of %s apis'),
auth_version)
return auth_version
self._LOG.error(_LE('No attempted versions [%s] supported by server') %
', '.join(_LIST_OF_VERSIONS_TO_ATTEMPT))
msg = _('No compatible apis supported by server')
raise ServiceError(msg)
def _json_request(self, method, path, **kwargs):
"""HTTP request helper used to make json requests.
@ -1612,24 +1589,8 @@ class _IdentityServer(object):
return response, data
def _fetch_cert_file(self, cert_type):
if not self._auth_version:
self._auth_version = self._choose_api_version()
version = None
if self._auth_version == 'v3.0':
if cert_type == 'signing':
cert_type = 'certificates'
path = '/OS-SIMPLE-CERT/' + cert_type
version = (3, 0)
else:
path = '/certificates/' + cert_type
version = (2, 0)
try:
response = self._adapter.get(
path, authenticated=False,
endpoint_filter={'version': version})
response = self._do_fetch_cert_file(cert_type)
except exceptions.HTTPError as e:
raise exceptions.CertificateConfigError(e.details)
if response.status_code != 200:
@ -1637,6 +1598,42 @@ class _IdentityServer(object):
return response.text
class _V2IdentityServer(_IdentityServer):
AUTH_VERSION = (2, 0)
def _do_verify_token(self, user_token):
return self._json_request('GET',
'/tokens/%s' % user_token,
authenticated=True)
def _do_fetch_cert_file(self, cert_type):
return self._adapter.get('/certificates/%s' % cert_type,
authenticated=False)
class _V3IdentityServer(_IdentityServer):
AUTH_VERSION = (3, 0)
def _do_verify_token(self, user_token):
path = '/auth/tokens'
if not self._include_service_catalog:
path += '?nocatalog'
return self._json_request('GET',
path,
authenticated=True,
headers={'X-Subject-Token': user_token})
def _do_fetch_cert_file(self, cert_type):
if cert_type == 'signing':
cert_type = 'certificates'
return self._adapter.get('/OS-SIMPLE-CERT/%s' % cert_type,
authenticated=False)
class _TokenCache(object):
"""Encapsulates the auth_token token cache functionality.
@ -1942,6 +1939,10 @@ def app_factory(global_conf, **local_conf):
return AuthProtocol(None, conf)
# NOTE(jamielennox): must be defined after identity server classes
_VERSIONS_TO_ATTEMPT = [_V3IdentityServer, _V2IdentityServer]
if __name__ == '__main__':
def echo_app(environ, start_response):
"""A WSGI application that echoes the CGI environment to the user."""

View File

@ -640,6 +640,7 @@ class CommonAuthTokenMiddlewareTest(object):
'auth_port': '1234',
'auth_protocol': 'http',
'auth_uri': None,
'auth_version': 'v3.0',
}
self.set_middleware(conf=conf)
expected_auth_uri = 'http://[2001:2013:1:f101::1]:1234'
@ -2119,7 +2120,9 @@ class DelayedAuthTests(BaseAuthTokenMiddlewareTest):
def test_header_in_401(self):
body = uuid.uuid4().hex
auth_uri = 'http://local.test'
conf = {'delay_auth_decision': 'True', 'auth_uri': auth_uri}
conf = {'delay_auth_decision': 'True',
'auth_version': 'v3.0',
'auth_uri': auth_uri}
self.fake_app = new_app('401 Unauthorized', body)
self.set_middleware(conf=conf)