Add plugin to support token-endpoint auth

The ksc auth plugins do not have support for the original
token-endpoint (aka token flow) auth where the user supplies
a token (possibly the Keystone admin_token) and an API endpoint.
This is used for bootstrapping Keystone but also has other uses
when a scoped user token is provided.

The api.auth:TokenEndpoint class is required to provide the
same interface methods so all of the special-case code branches
to support token-endpoint can be removed.

Some additional cleanups related to ClientManager and creating
the Compute client also were done to streamline using sessions.

Change-Id: I1a6059afa845a591eff92567ca346c09010a93af
This commit is contained in:
Dean Troyer 2014-10-09 15:16:07 -05:00
parent 0c77a9fe8b
commit c3c6edbe8a
5 changed files with 124 additions and 58 deletions

View File

@ -18,6 +18,8 @@ import logging
import stevedore import stevedore
from oslo.config import cfg
from keystoneclient.auth import base from keystoneclient.auth import base
from openstackclient.common import exceptions as exc from openstackclient.common import exceptions as exc
@ -53,14 +55,14 @@ for plugin in PLUGIN_LIST:
) )
def _guess_authentication_method(options): def select_auth_plugin(options):
"""If no auth plugin was specified, pick one based on other options""" """If no auth plugin was specified, pick one based on other options"""
if options.os_url:
# service token authentication, do nothing
return
auth_plugin = None auth_plugin = None
if options.os_password: if options.os_url and options.os_token:
# service token authentication
auth_plugin = 'token_endpoint'
elif options.os_password:
if options.os_identity_api_version == '3': if options.os_identity_api_version == '3':
auth_plugin = 'v3password' auth_plugin = 'v3password'
elif options.os_identity_api_version == '2.0': elif options.os_identity_api_version == '2.0':
@ -83,14 +85,13 @@ def _guess_authentication_method(options):
) )
LOG.debug("No auth plugin selected, picking %s from other " LOG.debug("No auth plugin selected, picking %s from other "
"options" % auth_plugin) "options" % auth_plugin)
options.os_auth_plugin = auth_plugin return auth_plugin
def build_auth_params(cmd_options): def build_auth_params(cmd_options):
auth_params = {} auth_params = {}
if cmd_options.os_url:
return {'token': cmd_options.os_token}
if cmd_options.os_auth_plugin: if cmd_options.os_auth_plugin:
LOG.debug('auth_plugin: %s', cmd_options.os_auth_plugin)
auth_plugin = base.get_plugin_class(cmd_options.os_auth_plugin) auth_plugin = base.get_plugin_class(cmd_options.os_auth_plugin)
plugin_options = auth_plugin.get_options() plugin_options = auth_plugin.get_options()
for option in plugin_options: for option in plugin_options:
@ -110,6 +111,7 @@ def build_auth_params(cmd_options):
None, None,
) )
else: else:
LOG.debug('no auth_plugin')
# delay the plugin choice, grab every option # delay the plugin choice, grab every option
plugin_options = set([o.replace('-', '_') for o in OPTIONS_LIST]) plugin_options = set([o.replace('-', '_') for o in OPTIONS_LIST])
for option in plugin_options: for option in plugin_options:
@ -178,3 +180,54 @@ def build_auth_plugins_option_parser(parser):
help=argparse.SUPPRESS, help=argparse.SUPPRESS,
) )
return parser return parser
class TokenEndpoint(base.BaseAuthPlugin):
"""Auth plugin to handle traditional token/endpoint usage
Implements the methods required to handle token authentication
with a user-specified token and service endpoint; no Identity calls
are made for re-scoping, service catalog lookups or the like.
The purpose of this plugin is to get rid of the special-case paths
in the code to handle this authentication format. Its primary use
is for bootstrapping the Keystone database.
"""
def __init__(self, url, token, **kwargs):
"""A plugin for static authentication with an existing token
:param string url: Service endpoint
:param string token: Existing token
"""
super(TokenEndpoint, self).__init__()
self.endpoint = url
self.token = token
def get_endpoint(self, session, **kwargs):
"""Return the supplied endpoint"""
return self.endpoint
def get_token(self, session):
"""Return the supplied token"""
return self.token
def get_auth_ref(self, session, **kwargs):
"""Stub this method for compatibility"""
return None
# Override this because it needs to be a class method...
@classmethod
def get_options(self):
options = super(TokenEndpoint, self).get_options()
options.extend([
# Maintain name 'url' for compatibility
cfg.StrOpt('url',
help='Specific service endpoint to use'),
cfg.StrOpt('token',
secret=True,
help='Authentication token to use'),
])
return options

View File

@ -54,9 +54,10 @@ class ClientManager(object):
return self._auth_params[name[1:]] return self._auth_params[name[1:]]
def __init__(self, auth_options, api_version=None, verify=True): def __init__(self, auth_options, api_version=None, verify=True):
# If no plugin is named by the user, select one based on
# the supplied options
if not auth_options.os_auth_plugin: if not auth_options.os_auth_plugin:
auth._guess_authentication_method(auth_options) auth_options.os_auth_plugin = auth.select_auth_plugin(auth_options)
self._auth_plugin = auth_options.os_auth_plugin self._auth_plugin = auth_options.os_auth_plugin
self._url = auth_options.os_url self._url = auth_options.os_url
@ -66,7 +67,7 @@ class ClientManager(object):
self._service_catalog = None self._service_catalog = None
self.timing = auth_options.timing self.timing = auth_options.timing
# For compatability until all clients can be updated # For compatibility until all clients can be updated
if 'project_name' in self._auth_params: if 'project_name' in self._auth_params:
self._project_name = self._auth_params['project_name'] self._project_name = self._auth_params['project_name']
elif 'tenant_name' in self._auth_params: elif 'tenant_name' in self._auth_params:
@ -86,8 +87,6 @@ class ClientManager(object):
root_logger = logging.getLogger('') root_logger = logging.getLogger('')
LOG.setLevel(root_logger.getEffectiveLevel()) LOG.setLevel(root_logger.getEffectiveLevel())
self.session = None
if not self._url:
LOG.debug('Using auth plugin: %s' % self._auth_plugin) LOG.debug('Using auth plugin: %s' % self._auth_plugin)
auth_plugin = base.get_plugin_class(self._auth_plugin) auth_plugin = base.get_plugin_class(self._auth_plugin)
self.auth = auth_plugin.load_from_options(**self._auth_params) self.auth = auth_plugin.load_from_options(**self._auth_params)
@ -100,13 +99,13 @@ class ClientManager(object):
) )
self.auth_ref = None self.auth_ref = None
if not self._auth_plugin.endswith("token") and not self._url: if 'token' not in self._auth_params:
LOG.debug("Populate other password flow attributes") LOG.debug("Get service catalog")
self.auth_ref = self.session.auth.get_auth_ref(self.session) self.auth_ref = self.auth.get_auth_ref(self.session)
self._token = self.session.auth.get_token(self.session)
self._service_catalog = self.auth_ref.service_catalog self._service_catalog = self.auth_ref.service_catalog
else:
self._token = self._auth_params.get('token') # This begone when clients no longer need it...
self._token = self.auth.get_token(self.session)
return return

View File

@ -44,33 +44,20 @@ def make_client(instance):
extensions = [extension.Extension('list_extensions', list_extensions)] extensions = [extension.Extension('list_extensions', list_extensions)]
client = compute_client( client = compute_client(
username=instance._username, session=instance.session,
api_key=instance._password,
project_id=instance._project_name,
auth_url=instance._auth_url,
cacert=instance._cacert,
insecure=instance._insecure,
region_name=instance._region_name,
# FIXME(dhellmann): get endpoint_type from option?
endpoint_type='publicURL',
extensions=extensions, extensions=extensions,
service_type=API_NAME,
# FIXME(dhellmann): what is service_name?
service_name='',
http_log_debug=http_log_debug, http_log_debug=http_log_debug,
timings=instance.timing, timings=instance.timing,
) )
# Populate the Nova client to skip another auth query to Identity # Populate the Nova client to skip another auth query to Identity
if instance._url: if 'token' not in instance._auth_params:
# token flow
client.client.management_url = instance._url
else:
# password flow # password flow
client.client.management_url = instance.get_endpoint_for_service_type( client.client.management_url = instance.get_endpoint_for_service_type(
API_NAME) API_NAME)
client.client.service_catalog = instance._service_catalog client.client.service_catalog = instance._service_catalog
client.client.auth_token = instance._token client.client.auth_token = instance.auth.get_token(instance.session)
return client return client

View File

@ -76,6 +76,31 @@ class TestClientManager(utils.TestCase):
url=fakes.AUTH_URL, url=fakes.AUTH_URL,
verb='GET') verb='GET')
def test_client_manager_token_endpoint(self):
client_manager = clientmanager.ClientManager(
auth_options=FakeOptions(os_token=fakes.AUTH_TOKEN,
os_url=fakes.AUTH_URL,
os_auth_plugin='token_endpoint'),
api_version=API_VERSION,
verify=True
)
self.assertEqual(
fakes.AUTH_URL,
client_manager._url,
)
self.assertEqual(
fakes.AUTH_TOKEN,
client_manager._token,
)
self.assertIsInstance(
client_manager.auth,
auth.TokenEndpoint,
)
self.assertFalse(client_manager._insecure)
self.assertTrue(client_manager._verify)
def test_client_manager_token(self): def test_client_manager_token(self):
client_manager = clientmanager.ClientManager( client_manager = clientmanager.ClientManager(
@ -176,8 +201,7 @@ class TestClientManager(utils.TestCase):
self.assertTrue(client_manager._verify) self.assertTrue(client_manager._verify)
self.assertEqual('cafile', client_manager._cacert) self.assertEqual('cafile', client_manager._cacert)
def _client_manager_guess_auth_plugin(self, auth_params, def _select_auth_plugin(self, auth_params, api_version, auth_plugin):
api_version, auth_plugin):
auth_params['os_auth_plugin'] = auth_plugin auth_params['os_auth_plugin'] = auth_plugin
auth_params['os_identity_api_version'] = api_version auth_params['os_identity_api_version'] = api_version
client_manager = clientmanager.ClientManager( client_manager = clientmanager.ClientManager(
@ -190,25 +214,25 @@ class TestClientManager(utils.TestCase):
client_manager._auth_plugin, client_manager._auth_plugin,
) )
def test_client_manager_guess_auth_plugin(self): def test_client_manager_select_auth_plugin(self):
# test token auth # test token auth
params = dict(os_token=fakes.AUTH_TOKEN, params = dict(os_token=fakes.AUTH_TOKEN,
os_auth_url=fakes.AUTH_URL) os_auth_url=fakes.AUTH_URL)
self._client_manager_guess_auth_plugin(params, '2.0', 'v2token') self._select_auth_plugin(params, '2.0', 'v2token')
self._client_manager_guess_auth_plugin(params, '3', 'v3token') self._select_auth_plugin(params, '3', 'v3token')
self._client_manager_guess_auth_plugin(params, 'XXX', 'token') self._select_auth_plugin(params, 'XXX', 'token')
# test service auth # test token/endpoint auth
params = dict(os_token=fakes.AUTH_TOKEN, os_url='test') params = dict(os_token=fakes.AUTH_TOKEN, os_url='test')
self._client_manager_guess_auth_plugin(params, 'XXX', '') self._select_auth_plugin(params, 'XXX', 'token_endpoint')
# test password auth # test password auth
params = dict(os_auth_url=fakes.AUTH_URL, params = dict(os_auth_url=fakes.AUTH_URL,
os_username=fakes.USERNAME, os_username=fakes.USERNAME,
os_password=fakes.PASSWORD) os_password=fakes.PASSWORD)
self._client_manager_guess_auth_plugin(params, '2.0', 'v2password') self._select_auth_plugin(params, '2.0', 'v2password')
self._client_manager_guess_auth_plugin(params, '3', 'v3password') self._select_auth_plugin(params, '3', 'v3password')
self._client_manager_guess_auth_plugin(params, 'XXX', 'password') self._select_auth_plugin(params, 'XXX', 'password')
def test_client_manager_guess_auth_plugin_failure(self): def test_client_manager_select_auth_plugin_failure(self):
self.assertRaises(exc.CommandError, self.assertRaises(exc.CommandError,
clientmanager.ClientManager, clientmanager.ClientManager,
auth_options=FakeOptions(os_auth_plugin=''), auth_options=FakeOptions(os_auth_plugin=''),

View File

@ -27,6 +27,9 @@ packages =
console_scripts = console_scripts =
openstack = openstackclient.shell:main openstack = openstackclient.shell:main
keystoneclient.auth.plugin =
token_endpoint = openstackclient.api.auth:TokenEndpoint
openstack.cli = openstack.cli =
module_list = openstackclient.common.module:ListModule module_list = openstackclient.common.module:ListModule