Convert authentication into a plugin

Extract all the information that is related to auth and make it into a
keystoneclient auth plugin. This will make it easier later to substitute
this auth plugin for another auth plugin.

Blueprint: pluggable-auth
Change-Id: Ibf9bc1dbf26f396bac09a96398a6467afc30f529
This commit is contained in:
Jamie Lennox 2014-08-21 12:42:35 +10:00
parent 365b3ea1d9
commit f21ccea42c
3 changed files with 241 additions and 109 deletions

View File

@ -169,6 +169,7 @@ import tempfile
import time import time
from keystoneclient import access from keystoneclient import access
from keystoneclient import auth
from keystoneclient.auth.identity import base as base_identity from keystoneclient.auth.identity import base as base_identity
from keystoneclient.auth.identity import v2 from keystoneclient.auth.identity import v2
from keystoneclient.auth import token_endpoint from keystoneclient.auth import token_endpoint
@ -213,33 +214,12 @@ from keystonemiddleware.openstack.common import memorycache
# options via CONF. # options via CONF.
_OPTS = [ _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', cfg.StrOpt('auth_uri',
default=None, default=None,
# FIXME(dolph): should be default='http://127.0.0.1:5000/v2.0/', # FIXME(dolph): should be default='http://127.0.0.1:5000/v2.0/',
# or (depending on client support) an unversioned, publicly # or (depending on client support) an unversioned, publicly
# accessible identity endpoint (see bug 1207517) # accessible identity endpoint (see bug 1207517)
help='Complete public Identity API endpoint.'), 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', cfg.StrOpt('auth_version',
default=None, default=None,
help='API version of the admin Identity API endpoint.'), help='API version of the admin Identity API endpoint.'),
@ -256,23 +236,6 @@ _OPTS = [
default=3, default=3,
help='How many times are we trying to reconnect when' help='How many times are we trying to reconnect when'
' communicating with Identity API Server.'), ' 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', cfg.StrOpt('cache',
default=None, default=None,
help='Env key for the swift cache.'), help='Env key for the swift cache.'),
@ -376,9 +339,9 @@ _OPTS = [
' should be set to a single value for better performance.'), ' should be set to a single value for better performance.'),
] ]
_AUTHTOKEN_GROUP = 'keystone_authtoken'
CONF = cfg.CONF 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'] _LIST_OF_VERSIONS_TO_ATTEMPT = ['v3.0', 'v2.0']
_HEADER_TEMPLATE = { _HEADER_TEMPLATE = {
@ -541,6 +504,121 @@ class _MiniResp(object):
self.headers.append(('Content-type', 'text/plain')) 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): class _UserAuthPlugin(base_identity.BaseIdentityPlugin):
"""The incoming authentication credentials. """The incoming authentication credentials.
@ -591,38 +669,6 @@ 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 ':' 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._session = self._session_factory()
self._http_request_max_retries = self._conf_get( 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 # NOTE(hrybacki): This and subsequent factory functions are part of a
# cleanup and better organization effort of AuthProtocol. # cleanup and better organization effort of AuthProtocol.
def _session_factory(self): 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( sess = session.Session.construct(dict(
cert=self._conf_get('certfile'), cert=self._conf_get('certfile'),
key=self._conf_get('keyfile'), key=self._conf_get('keyfile'),
@ -1224,26 +1274,23 @@ class AuthProtocol(object):
insecure=self._conf_get('insecure'), insecure=self._conf_get('insecure'),
timeout=self._conf_get('http_connect_timeout') 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') # NOTE(jamielennox): Loading AuthTokenPlugin here should be exactly the
if admin_token: # same as calling _AuthTokenPlugin.load_from_conf_options(CONF, GROUP)
self._LOG.warning( # however we can't do that because we have to use _conf_get to support
"The admin_token option in the auth_token middleware is " # the paste.ini options.
"deprecated and should not be used. The admin_user and " sess.auth = _AuthTokenPlugin.load_from_options(
"admin_password options should be used instead. The " auth_host=self._conf_get('auth_host'),
"admin_token option may be removed in a future release.") auth_port=int(self._conf_get('auth_port')),
sess.auth = token_endpoint.Token(auth_url, admin_token) auth_protocol=self._conf_get('auth_protocol'),
else: auth_admin_prefix=self._conf_get('auth_admin_prefix'),
sess.auth = v2.Password( admin_user=self._conf_get('admin_user'),
auth_url, admin_password=self._conf_get('admin_password'),
username=self._conf_get('admin_user'), admin_tenant_name=self._conf_get('admin_tenant_name'),
password=self._conf_get('admin_password'), admin_token=self._conf_get('admin_token'),
tenant_name=self._conf_get('admin_tenant_name')) identity_uri=self._conf_get('identity_uri'),
log=self._LOG)
return sess return sess
def _identity_server_factory(self): def _identity_server_factory(self):
@ -1251,7 +1298,6 @@ class AuthProtocol(object):
self._LOG, self._LOG,
self._session, self._session,
include_service_catalog=self._include_service_catalog, include_service_catalog=self._include_service_catalog,
identity_uri=self._identity_uri,
auth_uri=self._conf_get('auth_uri'), auth_uri=self._conf_get('auth_uri'),
http_request_max_retries=self._http_request_max_retries, http_request_max_retries=self._http_request_max_retries,
auth_version=self._conf_get('auth_version')) auth_version=self._conf_get('auth_version'))
@ -1351,19 +1397,14 @@ class _IdentityServer(object):
""" """
def __init__(self, log, session, include_service_catalog=None, def __init__(self, log, session, include_service_catalog=None,
identity_uri=None, auth_uri=None, auth_uri=None, http_request_max_retries=None,
http_request_max_retries=None, auth_version=None): auth_version=None):
self._LOG = log self._LOG = log
self._include_service_catalog = include_service_catalog self._include_service_catalog = include_service_catalog
self._req_auth_version = auth_version 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 self._session = session
if self.auth_uri is None: if auth_uri is None:
self._LOG.warning( self._LOG.warning(
'Configuring auth_uri to point to the public identity ' 'Configuring auth_uri to point to the public identity '
'endpoint is required; clients may not be able to ' '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 # FIXME(dolph): drop support for this fallback behavior as
# documented in bug 1207517. # documented in bug 1207517.
# NOTE(jamielennox): we urljoin '/' to get just the base URI as auth_uri = session.get_endpoint(interface=auth.AUTH_INTERFACE)
# this is the original behaviour.
self.auth_uri = urllib.parse.urljoin(self._identity_uri, '/')
self.auth_uri = self.auth_uri.rstrip('/')
# 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._auth_version = None
self._http_request_max_retries = http_request_max_retries self._http_request_max_retries = http_request_max_retries
@ -1519,13 +1564,16 @@ class _IdentityServer(object):
:raise ServerError when unable to communicate with keystone :raise ServerError when unable to communicate with keystone
""" """
url = '%s/%s' % (self._identity_uri, path.lstrip('/'))
RETRIES = self._http_request_max_retries RETRIES = self._http_request_max_retries
retry = 0 retry = 0
endpoint_filter = kwargs.setdefault('endpoint_filter', {})
endpoint_filter.setdefault('service_type', 'identity')
endpoint_filter.setdefault('interface', 'admin')
while True: while True:
try: try:
response = self._session.request(url, method, **kwargs) response = self._session.request(path, method, **kwargs)
break break
except exceptions.HTTPError: except exceptions.HTTPError:
# NOTE(hrybacki): unlike the requests library that return # NOTE(hrybacki): unlike the requests library that return

View File

@ -22,7 +22,9 @@ import keystonemiddleware.auth_token
auth_token_opts = [ auth_token_opts = [
('keystone_authtoken', keystonemiddleware.auth_token._OPTS) (keystonemiddleware.auth_token._AUTHTOKEN_GROUP,
keystonemiddleware.auth_token._OPTS +
keystonemiddleware.auth_token._AuthTokenPlugin.get_options())
] ]

View File

@ -15,6 +15,7 @@
import calendar import calendar
import datetime import datetime
import json import json
import logging
import os import os
import shutil import shutil
import stat import stat
@ -24,6 +25,7 @@ import uuid
import fixtures import fixtures
from keystoneclient import access from keystoneclient import access
from keystoneclient import auth
from keystoneclient.common import cms from keystoneclient.common import cms
from keystoneclient import exceptions from keystoneclient import exceptions
from keystoneclient import fixture from keystoneclient import fixture
@ -633,7 +635,7 @@ class GeneralAuthTokenMiddlewareTest(BaseAuthTokenMiddlewareTest,
middleware._token_revocation_list_cache_timeout) middleware._token_revocation_list_cache_timeout)
self.assertEqual(False, middleware._include_service_catalog) self.assertEqual(False, middleware._include_service_catalog)
self.assertEqual('https://keystone.example.com:1234', self.assertEqual('https://keystone.example.com:1234',
middleware._identity_uri) middleware._session.auth._identity_uri)
self.assertEqual('0', middleware._conf['nonexsit_option']) self.assertEqual('0', middleware._conf['nonexsit_option'])
def test_conf_values_type_convert_with_wrong_value(self): def test_conf_values_type_convert_with_wrong_value(self):
@ -2407,5 +2409,85 @@ class v3CompositeAuthTests(BaseAuthTokenMiddlewareTest,
return response 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): def load_tests(loader, tests, pattern):
return testresources.OptimisingTestSuite(tests) return testresources.OptimisingTestSuite(tests)