From cd58da52134069f94e7efb82685882d5d3aa122f Mon Sep 17 00:00:00 2001 From: Swapnil Kulkarni <swapnilkulkarni2608@gmail.com> Date: Sat, 25 Jan 2014 08:25:25 +0530 Subject: [PATCH] Remove RAX-specific auth in troveclient Author: Swapnil Kulkarni <swapnilkulkarni2608@gmail.com> Co-Authored-By: Nikhil Manchanda <SlickNik@gmail.com> Co-Authored-By: Craig Vyvial <cp16net@gmail.com> Change-Id: I250777890a1f5240c5f14290cf02eb5a7b34b434 Closes-Bug: #966329 --- troveclient/auth_plugin.py | 107 ++++++++++++++++++++++++++++++++ troveclient/client.py | 33 +++++----- troveclient/shell.py | 72 +++++++++++++-------- troveclient/tests/test_shell.py | 4 +- 4 files changed, 173 insertions(+), 43 deletions(-) create mode 100644 troveclient/auth_plugin.py diff --git a/troveclient/auth_plugin.py b/troveclient/auth_plugin.py new file mode 100644 index 00000000..e8a7d976 --- /dev/null +++ b/troveclient/auth_plugin.py @@ -0,0 +1,107 @@ +# Copyright 2014 Rackspace +# Copyright 2013 OpenStack Foundation +# Copyright 2013 Spanish National Research Council. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import logging + +import pkg_resources +import six + +from troveclient import exceptions +from troveclient.openstack.common.gettextutils import _ # noqa + + +logger = logging.getLogger(__name__) + + +_discovered_plugins = {} + + +def discover_auth_systems(): + """Discover the available auth-systems. + + This won't take into account the old style auth-systems. + """ + ep_name = 'openstack.client.auth_plugin' + for ep in pkg_resources.iter_entry_points(ep_name): + try: + auth_plugin = ep.load() + except (ImportError, pkg_resources.UnknownExtra, AttributeError) as e: + logger.debug(_("ERROR: Cannot load auth plugin %s") % ep.name) + logger.debug(e, exc_info=1) + else: + _discovered_plugins[ep.name] = auth_plugin + + +def load_auth_system_opts(parser): + """Load options needed by the available auth-systems into a parser. + + This function will try to populate the parser with options from the + available plugins. + """ + for name, auth_plugin in six.iteritems(_discovered_plugins): + add_opts_fn = getattr(auth_plugin, "add_opts", None) + if add_opts_fn: + group = parser.add_argument_group("Auth-system '%s' options" % + name) + add_opts_fn(group) + + +def load_plugin(auth_system): + if auth_system in _discovered_plugins: + return _discovered_plugins[auth_system]() + + raise exceptions.AuthSystemNotFound(auth_system) + + +class BaseAuthPlugin(object): + """Base class for authentication plugins. + + An authentication plugin needs to override at least the authenticate + method to be a valid plugin. + """ + def __init__(self): + self.opts = {} + + def get_auth_url(self): + """Return the auth url for the plugin (if any).""" + return None + + @staticmethod + def add_opts(parser): + """Populate and return the parser with the options for this plugin. + + If the plugin does not need any options, it should return the same + parser untouched. + """ + return parser + + def parse_opts(self, args): + """Parse the actual auth-system options if any. + + This method is expected to populate the attribute self.opts with a + dict containing the options and values needed to make authentication. + If the dict is empty, the client should assume that it needs the same + options as the 'keystone' auth system (i.e. os_username and + os_password). + + Returns the self.opts dict. + """ + return self.opts + + def authenticate(self, cls, auth_url): + """Authenticate using plugin defined method.""" + raise exceptions.AuthSystemNotFound(self.auth_system) diff --git a/troveclient/client.py b/troveclient/client.py index d9dcc48f..ecfaf598 100644 --- a/troveclient/client.py +++ b/troveclient/client.py @@ -22,7 +22,6 @@ OpenStack Client interface. Handles the REST calls and responses. from __future__ import print_function import logging -import os import requests from keystoneclient import adapter @@ -89,7 +88,16 @@ class HTTPClient(TroveClientMixin): self.password = password self.projectid = projectid self.tenant_id = tenant_id - self.auth_url = auth_url.rstrip('/') + + if auth_system and auth_system != 'keystone' and not auth_plugin: + raise exceptions.AuthSystemNotFound(auth_system) + + if not auth_url and auth_system and auth_system != 'keystone': + auth_url = auth_plugin.get_auth_url() + if not auth_url: + raise exceptions.EndpointNotFound() + + self.auth_url = auth_url.rstrip('/') if auth_url else auth_url self.version = 'v1' self.region_name = region_name self.endpoint_type = endpoint_type @@ -105,6 +113,8 @@ class HTTPClient(TroveClientMixin): self.proxy_tenant_id = proxy_tenant_id self.timeout = timeout self.bypass_url = bypass_url + self.auth_system = auth_system + self.auth_plugin = auth_plugin if insecure: self.verify_cert = False @@ -326,10 +336,10 @@ class HTTPClient(TroveClientMixin): auth_url = self.auth_url if self.version == "v2.0": while auth_url: - if "TROVE_RAX_AUTH" in os.environ: - auth_url = self._rax_auth(auth_url) - else: + if not self.auth_system or self.auth_system == 'keystone': auth_url = self._v2_auth(auth_url) + else: + auth_url = self._plugin_auth(auth_url) # Are we acting on behalf of another user via an # existing token? If so, our actual endpoints may @@ -357,6 +367,9 @@ class HTTPClient(TroveClientMixin): if self.bypass_url is not None and self.bypass_url != '': self.management_url = self.bypass_url + def _plugin_auth(self, auth_url): + return self.auth_plugin.authenticate(self, auth_url) + def _v1_auth(self, url): if self.proxy_token: raise exceptions.NoTokenLookupException() @@ -393,16 +406,6 @@ class HTTPClient(TroveClientMixin): self._authenticate(url, body) - def _rax_auth(self, url): - """Authenticate against the Rackspace auth service.""" - body = {"auth": { - "RAX-KSKEY:apiKeyCredentials": { - "username": self.user, - "apiKey": self.password, - "tenantName": self.projectid}}} - - self._authenticate(url, body) - def _authenticate(self, url, body): """Authenticate and extract the service catalog.""" token_url = url + "/tokens" diff --git a/troveclient/shell.py b/troveclient/shell.py index 3738d30d..fb8d4269 100644 --- a/troveclient/shell.py +++ b/troveclient/shell.py @@ -21,6 +21,7 @@ Command-line interface to the OpenStack Trove API. from __future__ import print_function import argparse +import getpass import glob import imp import itertools @@ -38,9 +39,9 @@ from keystoneclient.auth.identity import v3 as identity from keystoneclient import session as ks_session import troveclient -import troveclient.extension - +import troveclient.auth_plugin from troveclient import client +import troveclient.extension from troveclient.openstack.common.apiclient import exceptions as exc from troveclient.openstack.common import gettextutils as gtu from troveclient.openstack.common.gettextutils import _ # noqa @@ -107,7 +108,10 @@ class OpenStackTroveShell(object): parser.add_argument('--os-auth-system', metavar='<auth-system>', - default=utils.env('OS_AUTH_SYSTEM')) + default=utils.env('OS_AUTH_SYSTEM'), + help='Defaults to env[OS_AUTH_SYSTEM].') + parser.add_argument('--os_auth_system', + help=argparse.SUPPRESS) parser.add_argument('--service-type', metavar='<service-type>', @@ -175,6 +179,9 @@ class OpenStackTroveShell(object): self._append_global_identity_args(parser) + # The auth-system-plugins might require some extra options + troveclient.auth_plugin.load_auth_system_opts(parser) + return parser def _append_global_identity_args(self, parser): @@ -332,6 +339,9 @@ class OpenStackTroveShell(object): self.setup_debugging(options.debug) self.options = options + # Discover available auth plugins + troveclient.auth_plugin.discover_auth_systems() + # build available subcommands based on version self.extensions = self._discover_extensions( options.os_database_api_version) @@ -356,17 +366,20 @@ class OpenStackTroveShell(object): self.do_bash_completion(args) return 0 - (os_username, os_password, os_tenant_name, os_auth_url, - os_region_name, os_tenant_id, endpoint_type, insecure, - service_type, service_name, database_service_name, - cacert, bypass_url, os_auth_system) = ( - args.os_username, args.os_password, - args.os_tenant_name, args.os_auth_url, - args.os_region_name, args.os_tenant_id, - args.endpoint_type, args.insecure, - args.service_type, args.service_name, - args.database_service_name, - args.os_cacert, args.bypass_url, args.os_auth_system) + os_username = args.os_username + os_password = args.os_password + os_tenant_name = args.os_tenant_name + os_auth_url = args.os_auth_url + os_region_name = args.os_region_name + os_tenant_id = args.os_tenant_id + os_auth_system = args.os_auth_system + endpoint_type = args.endpoint_type + insecure = args.insecure + service_type = args.service_type + service_name = args.service_name + database_service_name = args.database_service_name + cacert = args.os_cacert + bypass_url = args.bypass_url if os_auth_system and os_auth_system != "keystone": auth_plugin = troveclient.auth_plugin.load_plugin(os_auth_system) @@ -384,20 +397,22 @@ class OpenStackTroveShell(object): # for os_username or os_password but for compatibility it is not. if not utils.isunauthenticated(args.func): - if not os_username: - raise exc.CommandError( - "You must provide a username " - "via either --os-username or env[OS_USERNAME]") + + if auth_plugin: + auth_plugin.parse_opts(args) + + if not auth_plugin or not auth_plugin.opts: + if not os_username: + raise exc.CommandError( + "You must provide a username " + "via either --os-username or env[OS_USERNAME]") if not os_password: - raise exc.CommandError("You must provide a password " - "via either --os-password or via " - "env[OS_PASSWORD]") + os_password = getpass.getpass() if not os_auth_url: - raise exc.CommandError( - "You must provide an auth url " - "via either --os-auth-url or env[OS_AUTH_URL]") + if os_auth_system and os_auth_system != 'keystone': + os_auth_url = auth_plugin.get_auth_url() # V3 stuff project_info_provided = (self.options.os_tenant_name or @@ -422,9 +437,12 @@ class OpenStackTroveShell(object): "(env[OS_PROJECT_DOMAIN_NAME])")) if not os_auth_url: - raise exc.CommandError( - "You must provide an auth url " - "via either --os-auth-url or env[OS_AUTH_URL]") + raise exc.CommandError("You must provide an auth url " + "via either --os-auth-url or " + "env[OS_AUTH_URL] or specify an " + "auth_system which defines a default " + "url with --os-auth-system or " + "env[OS_AUTH_SYSTEM]") use_session = True if auth_plugin or bypass_url: diff --git a/troveclient/tests/test_shell.py b/troveclient/tests/test_shell.py index aaca9e16..853f2a4b 100644 --- a/troveclient/tests/test_shell.py +++ b/troveclient/tests/test_shell.py @@ -150,7 +150,9 @@ class ShellTest(testtools.TestCase): def test_no_auth_url(self): required = ('You must provide an auth url' - ' via either --os-auth-url or env[OS_AUTH_URL]',) + ' via either --os-auth-url or env[OS_AUTH_URL] ' + 'or specify an auth_system which defines a default ' + 'url with --os-auth-system or env[OS_AUTH_SYSTEM]',) self.make_env(exclude='OS_AUTH_URL') try: self.shell('list')