From c3c6edbe8a083aef0fb6aea3cb461ff8e715fc59 Mon Sep 17 00:00:00 2001 From: Dean Troyer Date: Thu, 9 Oct 2014 15:16:07 -0500 Subject: [PATCH] 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 --- openstackclient/api/auth.py | 69 ++++++++++++++++--- openstackclient/common/clientmanager.py | 41 ++++++----- openstackclient/compute/client.py | 21 ++---- .../tests/common/test_clientmanager.py | 48 +++++++++---- setup.cfg | 3 + 5 files changed, 124 insertions(+), 58 deletions(-) diff --git a/openstackclient/api/auth.py b/openstackclient/api/auth.py index 2bd5271f7d..e33b72d575 100644 --- a/openstackclient/api/auth.py +++ b/openstackclient/api/auth.py @@ -18,6 +18,8 @@ import logging import stevedore +from oslo.config import cfg + from keystoneclient.auth import base 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 options.os_url: - # service token authentication, do nothing - return 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': auth_plugin = 'v3password' 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 " "options" % auth_plugin) - options.os_auth_plugin = auth_plugin + return auth_plugin def build_auth_params(cmd_options): auth_params = {} - if cmd_options.os_url: - return {'token': cmd_options.os_token} 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) plugin_options = auth_plugin.get_options() for option in plugin_options: @@ -110,6 +111,7 @@ def build_auth_params(cmd_options): None, ) else: + LOG.debug('no auth_plugin') # delay the plugin choice, grab every option plugin_options = set([o.replace('-', '_') for o in OPTIONS_LIST]) for option in plugin_options: @@ -178,3 +180,54 @@ def build_auth_plugins_option_parser(parser): help=argparse.SUPPRESS, ) 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 diff --git a/openstackclient/common/clientmanager.py b/openstackclient/common/clientmanager.py index 0542b47362..bcb81990ad 100644 --- a/openstackclient/common/clientmanager.py +++ b/openstackclient/common/clientmanager.py @@ -54,9 +54,10 @@ class ClientManager(object): return self._auth_params[name[1:]] 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: - 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._url = auth_options.os_url @@ -66,7 +67,7 @@ class ClientManager(object): self._service_catalog = None 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: self._project_name = self._auth_params['project_name'] elif 'tenant_name' in self._auth_params: @@ -86,27 +87,25 @@ class ClientManager(object): root_logger = logging.getLogger('') LOG.setLevel(root_logger.getEffectiveLevel()) - self.session = None - if not self._url: - LOG.debug('Using auth plugin: %s' % self._auth_plugin) - auth_plugin = base.get_plugin_class(self._auth_plugin) - self.auth = auth_plugin.load_from_options(**self._auth_params) - # needed by SAML authentication - request_session = requests.session() - self.session = session.Session( - auth=self.auth, - session=request_session, - verify=verify, - ) + LOG.debug('Using auth plugin: %s' % self._auth_plugin) + auth_plugin = base.get_plugin_class(self._auth_plugin) + self.auth = auth_plugin.load_from_options(**self._auth_params) + # needed by SAML authentication + request_session = requests.session() + self.session = session.Session( + auth=self.auth, + session=request_session, + verify=verify, + ) self.auth_ref = None - if not self._auth_plugin.endswith("token") and not self._url: - LOG.debug("Populate other password flow attributes") - self.auth_ref = self.session.auth.get_auth_ref(self.session) - self._token = self.session.auth.get_token(self.session) + if 'token' not in self._auth_params: + LOG.debug("Get service catalog") + self.auth_ref = self.auth.get_auth_ref(self.session) 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 diff --git a/openstackclient/compute/client.py b/openstackclient/compute/client.py index dc50507eb2..6c03d24e3f 100644 --- a/openstackclient/compute/client.py +++ b/openstackclient/compute/client.py @@ -44,33 +44,20 @@ def make_client(instance): extensions = [extension.Extension('list_extensions', list_extensions)] client = compute_client( - username=instance._username, - 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', + session=instance.session, extensions=extensions, - service_type=API_NAME, - # FIXME(dhellmann): what is service_name? - service_name='', http_log_debug=http_log_debug, timings=instance.timing, ) # Populate the Nova client to skip another auth query to Identity - if instance._url: - # token flow - client.client.management_url = instance._url - else: + if 'token' not in instance._auth_params: # password flow client.client.management_url = instance.get_endpoint_for_service_type( API_NAME) 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 diff --git a/openstackclient/tests/common/test_clientmanager.py b/openstackclient/tests/common/test_clientmanager.py index 18461fb7e8..5ec86d595d 100644 --- a/openstackclient/tests/common/test_clientmanager.py +++ b/openstackclient/tests/common/test_clientmanager.py @@ -76,6 +76,31 @@ class TestClientManager(utils.TestCase): url=fakes.AUTH_URL, 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): client_manager = clientmanager.ClientManager( @@ -176,8 +201,7 @@ class TestClientManager(utils.TestCase): self.assertTrue(client_manager._verify) self.assertEqual('cafile', client_manager._cacert) - def _client_manager_guess_auth_plugin(self, auth_params, - api_version, auth_plugin): + def _select_auth_plugin(self, auth_params, api_version, auth_plugin): auth_params['os_auth_plugin'] = auth_plugin auth_params['os_identity_api_version'] = api_version client_manager = clientmanager.ClientManager( @@ -190,25 +214,25 @@ class TestClientManager(utils.TestCase): client_manager._auth_plugin, ) - def test_client_manager_guess_auth_plugin(self): + def test_client_manager_select_auth_plugin(self): # test token auth params = dict(os_token=fakes.AUTH_TOKEN, os_auth_url=fakes.AUTH_URL) - self._client_manager_guess_auth_plugin(params, '2.0', 'v2token') - self._client_manager_guess_auth_plugin(params, '3', 'v3token') - self._client_manager_guess_auth_plugin(params, 'XXX', 'token') - # test service auth + self._select_auth_plugin(params, '2.0', 'v2token') + self._select_auth_plugin(params, '3', 'v3token') + self._select_auth_plugin(params, 'XXX', 'token') + # test token/endpoint auth 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 params = dict(os_auth_url=fakes.AUTH_URL, os_username=fakes.USERNAME, os_password=fakes.PASSWORD) - self._client_manager_guess_auth_plugin(params, '2.0', 'v2password') - self._client_manager_guess_auth_plugin(params, '3', 'v3password') - self._client_manager_guess_auth_plugin(params, 'XXX', 'password') + self._select_auth_plugin(params, '2.0', 'v2password') + self._select_auth_plugin(params, '3', 'v3password') + 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, clientmanager.ClientManager, auth_options=FakeOptions(os_auth_plugin=''), diff --git a/setup.cfg b/setup.cfg index dc85967f42..d9cced1523 100644 --- a/setup.cfg +++ b/setup.cfg @@ -27,6 +27,9 @@ packages = console_scripts = openstack = openstackclient.shell:main +keystoneclient.auth.plugin = + token_endpoint = openstackclient.api.auth:TokenEndpoint + openstack.cli = module_list = openstackclient.common.module:ListModule