diff --git a/keystonemiddleware/auth_token.py b/keystonemiddleware/auth_token.py index 50ce3a82..85ca87d1 100644 --- a/keystonemiddleware/auth_token.py +++ b/keystonemiddleware/auth_token.py @@ -169,6 +169,7 @@ import tempfile import time from keystoneclient import access +from keystoneclient import auth from keystoneclient.auth.identity import base as base_identity from keystoneclient.auth.identity import v2 from keystoneclient.auth import token_endpoint @@ -213,33 +214,12 @@ from keystonemiddleware.openstack.common import memorycache # options via CONF. _OPTS = [ - cfg.StrOpt('auth_admin_prefix', - default='', - help='Prefix to prepend at the beginning of the path. ' - 'Deprecated, use identity_uri.'), - cfg.StrOpt('auth_host', - default='127.0.0.1', - help='Host providing the admin Identity API endpoint. ' - 'Deprecated, use identity_uri.'), - cfg.IntOpt('auth_port', - default=35357, - help='Port of the admin Identity API endpoint. ' - 'Deprecated, use identity_uri.'), - cfg.StrOpt('auth_protocol', - default='https', - help='Protocol of the admin Identity API endpoint ' - '(http or https). Deprecated, use identity_uri.'), cfg.StrOpt('auth_uri', default=None, # FIXME(dolph): should be default='http://127.0.0.1:5000/v2.0/', # or (depending on client support) an unversioned, publicly # accessible identity endpoint (see bug 1207517) help='Complete public Identity API endpoint.'), - cfg.StrOpt('identity_uri', - default=None, - help='Complete admin Identity API endpoint. This should ' - 'specify the unversioned root endpoint ' - 'e.g. https://localhost:35357/'), cfg.StrOpt('auth_version', default=None, help='API version of the admin Identity API endpoint.'), @@ -256,23 +236,6 @@ _OPTS = [ default=3, help='How many times are we trying to reconnect when' ' communicating with Identity API Server.'), - cfg.StrOpt('admin_token', - secret=True, - help='This option is deprecated and may be removed in a future' - ' release. Single shared secret with the Keystone configuration' - ' used for bootstrapping a Keystone installation, or otherwise' - ' bypassing the normal authentication process. This option' - ' should not be used, use `admin_user` and `admin_password`' - ' instead.'), - cfg.StrOpt('admin_user', - help='Keystone account username.'), - cfg.StrOpt('admin_password', - secret=True, - help='Keystone account password.'), - cfg.StrOpt('admin_tenant_name', - default='admin', - help='Keystone service account tenant name to validate' - ' user tokens.'), cfg.StrOpt('cache', default=None, help='Env key for the swift cache.'), @@ -376,9 +339,9 @@ _OPTS = [ ' should be set to a single value for better performance.'), ] +_AUTHTOKEN_GROUP = 'keystone_authtoken' CONF = cfg.CONF -CONF.register_opts(_OPTS, group='keystone_authtoken') - +CONF.register_opts(_OPTS, group=_AUTHTOKEN_GROUP) _LIST_OF_VERSIONS_TO_ATTEMPT = ['v3.0', 'v2.0'] _HEADER_TEMPLATE = { @@ -541,6 +504,121 @@ class _MiniResp(object): self.headers.append(('Content-type', 'text/plain')) +class _AuthTokenPlugin(auth.BaseAuthPlugin): + + def __init__(self, auth_host, auth_port, auth_protocol, auth_admin_prefix, + admin_user, admin_password, admin_tenant_name, admin_token, + identity_uri, log): + # 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 identity_uri: + log.warning('Configuring admin URI using auth fragments. ' + 'This is deprecated, use \'identity_uri\'' + ' instead.') + + if ':' in 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 + + identity_uri = '%s://%s:%s' % (auth_protocol, + auth_host, + auth_port) + + if auth_admin_prefix: + identity_uri = '%s/%s' % (identity_uri, + auth_admin_prefix.strip('/')) + + self._identity_uri = identity_uri.rstrip('/') + + # 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 + + if admin_token: + 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._plugin = token_endpoint.Token(auth_url, admin_token) + else: + self._plugin = v2.Password(auth_url, + username=admin_user, + password=admin_password, + tenant_name=admin_tenant_name) + + self._LOG = log + + def get_token(self, *args, **kwargs): + return self._plugin.get_token(*args, **kwargs) + + def get_endpoint(self, session, interface=None, **kwargs): + # FIXME(jamielennox) for now the paths are always set up such that they + # expect the unversioned endpoint to be returned as the base. This will + # change shortly. + + return self._identity_uri + + def invalidate(self): + return self._plugin.invalidate() + + @classmethod + def get_options(cls): + options = super(_AuthTokenPlugin, cls).get_options() + + options.extend([ + cfg.StrOpt('auth_admin_prefix', + default='', + help='Prefix to prepend at the beginning of the path. ' + 'Deprecated, use identity_uri.'), + cfg.StrOpt('auth_host', + default='127.0.0.1', + help='Host providing the admin Identity API endpoint. ' + 'Deprecated, use identity_uri.'), + cfg.IntOpt('auth_port', + default=35357, + help='Port of the admin Identity API endpoint. ' + 'Deprecated, use identity_uri.'), + cfg.StrOpt('auth_protocol', + default='https', + help='Protocol of the admin Identity API endpoint ' + '(http or https). Deprecated, use identity_uri.'), + cfg.StrOpt('identity_uri', + default=None, + help='Complete admin Identity API endpoint. This ' + 'should specify the unversioned root endpoint ' + 'e.g. https://localhost:35357/'), + cfg.StrOpt('admin_token', + secret=True, + help='This option is deprecated and may be removed in ' + 'a future release. Single shared secret with the ' + 'Keystone configuration used for bootstrapping a ' + 'Keystone installation, or otherwise bypassing ' + 'the normal authentication process. This option ' + 'should not be used, use `admin_user` and ' + '`admin_password` instead.'), + cfg.StrOpt('admin_user', + help='Keystone account username.'), + cfg.StrOpt('admin_password', + secret=True, + help='Keystone account password.'), + cfg.StrOpt('admin_tenant_name', + default='admin', + help='Keystone service account tenant name to validate' + ' user tokens.'), + ]) + + return options + + +_AuthTokenPlugin.register_conf_options(CONF, _AUTHTOKEN_GROUP) + + class _UserAuthPlugin(base_identity.BaseIdentityPlugin): """The incoming authentication credentials. @@ -591,38 +669,6 @@ 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 ':' in 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( @@ -1217,6 +1263,10 @@ class AuthProtocol(object): # NOTE(hrybacki): This and subsequent factory functions are part of a # cleanup and better organization effort of AuthProtocol. def _session_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'), @@ -1224,26 +1274,23 @@ class AuthProtocol(object): 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')) + # 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. + sess.auth = _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) + return sess def _identity_server_factory(self): @@ -1251,7 +1298,6 @@ class AuthProtocol(object): 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')) @@ -1351,19 +1397,14 @@ class _IdentityServer(object): """ def __init__(self, log, session, include_service_catalog=None, - identity_uri=None, auth_uri=None, - http_request_max_retries=None, auth_version=None): + auth_uri=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 - self._session = session - if self.auth_uri is None: + if 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 ' @@ -1371,11 +1412,15 @@ class _IdentityServer(object): # 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('/') + auth_uri = session.get_endpoint(interface=auth.AUTH_INTERFACE) + # NOTE(jamielennox): This weird stripping of the prefix hack is + # only relevant to the legacy case. We urljoin '/' to get just the + # base URI as this is the original behaviour. + if isinstance(session.auth, _AuthTokenPlugin): + auth_uri = urllib.parse.urljoin(auth_uri, '/').rstrip('/') + + self.auth_uri = auth_uri self._auth_version = None self._http_request_max_retries = http_request_max_retries @@ -1519,13 +1564,16 @@ class _IdentityServer(object): :raise ServerError when unable to communicate with keystone """ - url = '%s/%s' % (self._identity_uri, path.lstrip('/')) - RETRIES = self._http_request_max_retries retry = 0 + + endpoint_filter = kwargs.setdefault('endpoint_filter', {}) + endpoint_filter.setdefault('service_type', 'identity') + endpoint_filter.setdefault('interface', 'admin') + while True: try: - response = self._session.request(url, method, **kwargs) + response = self._session.request(path, method, **kwargs) break except exceptions.HTTPError: # NOTE(hrybacki): unlike the requests library that return diff --git a/keystonemiddleware/opts.py b/keystonemiddleware/opts.py index 6abea9d2..33be3db7 100644 --- a/keystonemiddleware/opts.py +++ b/keystonemiddleware/opts.py @@ -22,7 +22,9 @@ import keystonemiddleware.auth_token auth_token_opts = [ - ('keystone_authtoken', keystonemiddleware.auth_token._OPTS) + (keystonemiddleware.auth_token._AUTHTOKEN_GROUP, + keystonemiddleware.auth_token._OPTS + + keystonemiddleware.auth_token._AuthTokenPlugin.get_options()) ] diff --git a/keystonemiddleware/tests/test_auth_token_middleware.py b/keystonemiddleware/tests/test_auth_token_middleware.py index 1e71ea46..4c7b38b9 100644 --- a/keystonemiddleware/tests/test_auth_token_middleware.py +++ b/keystonemiddleware/tests/test_auth_token_middleware.py @@ -15,6 +15,7 @@ import calendar import datetime import json +import logging import os import shutil import stat @@ -24,6 +25,7 @@ import uuid import fixtures from keystoneclient import access +from keystoneclient import auth from keystoneclient.common import cms from keystoneclient import exceptions from keystoneclient import fixture @@ -633,7 +635,7 @@ class GeneralAuthTokenMiddlewareTest(BaseAuthTokenMiddlewareTest, middleware._token_revocation_list_cache_timeout) self.assertEqual(False, middleware._include_service_catalog) self.assertEqual('https://keystone.example.com:1234', - middleware._identity_uri) + middleware._session.auth._identity_uri) self.assertEqual('0', middleware._conf['nonexsit_option']) def test_conf_values_type_convert_with_wrong_value(self): @@ -2407,5 +2409,85 @@ class v3CompositeAuthTests(BaseAuthTokenMiddlewareTest, return response +class DefaultAuthPluginTests(testtools.TestCase): + + def new_plugin(self, auth_host=None, auth_port=None, auth_protocol=None, + auth_admin_prefix=None, admin_user=None, + admin_password=None, admin_tenant_name=None, + admin_token=None, identity_uri=None, log=None): + if not log: + log = self.logger + + return auth_token._AuthTokenPlugin.load_from_options( + auth_host=auth_host, + auth_port=auth_port, + auth_protocol=auth_protocol, + auth_admin_prefix=auth_admin_prefix, + admin_user=admin_user, + admin_password=admin_password, + admin_tenant_name=admin_tenant_name, + admin_token=admin_token, + identity_uri=identity_uri, + log=log) + + def setUp(self): + super(DefaultAuthPluginTests, self).setUp() + + self.stream = six.StringIO() + self.logger = logging.getLogger(__name__) + self.session = session.Session() + self.requests = self.useFixture(rm_fixture.Fixture()) + + def test_auth_uri_from_fragments(self): + auth_protocol = 'http' + auth_host = 'testhost' + auth_port = 8888 + auth_admin_prefix = 'admin' + + expected = '%s://%s:%d/admin' % (auth_protocol, auth_host, auth_port) + + plugin = self.new_plugin(auth_host=auth_host, + auth_protocol=auth_protocol, + auth_port=auth_port, + auth_admin_prefix=auth_admin_prefix) + + self.assertEqual(expected, + plugin.get_endpoint(self.session, + interface=auth.AUTH_INTERFACE)) + + def test_identity_uri_overrides_fragments(self): + identity_uri = 'http://testhost:8888/admin' + plugin = self.new_plugin(identity_uri=identity_uri, + auth_host='anotherhost', + auth_port=9999, + auth_protocol='ftp') + + self.assertEqual(identity_uri, + plugin.get_endpoint(self.session, + interface=auth.AUTH_INTERFACE)) + + def test_with_admin_token(self): + token = uuid.uuid4().hex + plugin = self.new_plugin(identity_uri='http://testhost:8888/admin', + admin_token=token) + self.assertEqual(token, plugin.get_token(self.session)) + + def test_with_user_pass(self): + base_uri = 'http://testhost:8888/admin' + token = fixture.V2Token() + admin_tenant_name = uuid.uuid4().hex + + self.requests.register_uri('POST', + base_uri + '/v2.0/tokens', + json=token) + + plugin = self.new_plugin(identity_uri=base_uri, + admin_user=uuid.uuid4().hex, + admin_password=uuid.uuid4().hex, + admin_tenant_name=admin_tenant_name) + + self.assertEqual(token.token_id, plugin.get_token(self.session)) + + def load_tests(loader, tests, pattern): return testresources.OptimisingTestSuite(tests)