diff options
author | Jamie Lennox <jamielennox@redhat.com> | 2014-03-11 15:59:10 +1000 |
---|---|---|
committer | Jamie Lennox <jamielennox@redhat.com> | 2014-07-07 14:15:10 +1000 |
commit | 5c91ede44768ebbb2fff12f9a7c93e63b9bbd56d (patch) | |
tree | 66943ba183ba7dfd770b26c6abe5ab30dca970ab | |
parent | 3e88c35cd72190c2529c9ccaaf38961a33f30744 (diff) | |
download | python-keystoneclient-5c91ede44768ebbb2fff12f9a7c93e63b9bbd56d.tar.gz |
Plugin loading from config objects
Provide a pattern for auth plugins to load themselves from a config
object. The first user of this will be auth_token middleware however it
is not likely to be the only user.
By doing this in an exportable way we are defining a single config file
format for specifying how to load a plugin for all services. We also
provide a standard way of retrieving a plugins options for loading via
other mechanisms.
Blueprint: standard-client-params
Change-Id: I353b26a1ffc04a20666e76f5bd2f1e6d7c19a22d
-rw-r--r-- | keystoneclient/auth/base.py | 46 | ||||
-rw-r--r-- | keystoneclient/auth/conf.py | 116 | ||||
-rw-r--r-- | keystoneclient/auth/identity/base.py | 11 | ||||
-rw-r--r-- | keystoneclient/auth/identity/v2.py | 37 | ||||
-rw-r--r-- | keystoneclient/auth/identity/v3.py | 44 | ||||
-rw-r--r-- | keystoneclient/auth/token_endpoint.py | 11 | ||||
-rw-r--r-- | keystoneclient/tests/auth/test_conf.py | 176 | ||||
-rw-r--r-- | keystoneclient/tests/auth/utils.py | 83 | ||||
-rw-r--r-- | requirements.txt | 1 | ||||
-rw-r--r-- | setup.cfg | 6 | ||||
-rw-r--r-- | test-requirements.txt | 1 |
11 files changed, 531 insertions, 1 deletions
diff --git a/keystoneclient/auth/base.py b/keystoneclient/auth/base.py index 4cd3737..e9d4eed 100644 --- a/keystoneclient/auth/base.py +++ b/keystoneclient/auth/base.py @@ -13,6 +13,31 @@ import abc import six +import stevedore + +from keystoneclient import exceptions + +PLUGIN_NAMESPACE = 'keystoneclient.auth.plugin' + + +def get_plugin_class(name): + """Retrieve a plugin class by its entrypoint name. + + :param str name: The name of the object to get. + + :returns: An auth plugin class. + + :raises exceptions.NoMatchingPlugin: if a plugin cannot be created. + """ + try: + mgr = stevedore.DriverManager(namespace=PLUGIN_NAMESPACE, + name=name, + invoke_on_load=False) + except RuntimeError: + msg = 'The plugin %s could not be found' % name + raise exceptions.NoMatchingPlugin(msg) + + return mgr.driver @six.add_metaclass(abc.ABCMeta) @@ -70,3 +95,24 @@ class BaseAuthPlugin(object): If nothing happens returns False to indicate give up. """ return False + + @classmethod + def get_options(cls): + """Return the list of parameters associated with the auth plugin. + + This list may be used to generate CLI or config arguments. + + :returns list: A list of Param objects describing available plugin + parameters. + """ + return [] + + @classmethod + def load_from_options(cls, **kwargs): + """Create a plugin from the arguments retrieved from get_options. + + A client can override this function to do argument validation or to + handle differences between the registered options and what is required + to create the plugin. + """ + return cls(**kwargs) diff --git a/keystoneclient/auth/conf.py b/keystoneclient/auth/conf.py new file mode 100644 index 0000000..b14627e --- /dev/null +++ b/keystoneclient/auth/conf.py @@ -0,0 +1,116 @@ +# 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. + +from oslo.config import cfg + +from keystoneclient.auth import base +from keystoneclient import exceptions + +_AUTH_PLUGIN_OPT = cfg.StrOpt('auth_plugin', help='Name of the plugin to load') + +_section_help = 'Config Section from which to load plugin specific options' +_AUTH_SECTION_OPT = cfg.StrOpt('auth_section', help=_section_help) + + +def get_common_conf_options(): + """Get the oslo.config options common for all auth plugins. + + These may be useful without being registered for config file generation + or to manipulate the options before registering them yourself. + + The options that are set are: + :auth_plugin: The name of the pluign to load. + :auth_section: The config file section to load options from. + + :returns: A list of oslo.config options. + """ + return [_AUTH_PLUGIN_OPT, _AUTH_SECTION_OPT] + + +def get_plugin_options(name): + """Get the oslo.config options for a specific plugin. + + This will be the list of config options that is registered and loaded by + the specified plugin. + + :returns: A list of oslo.config options. + """ + return base.get_plugin_class(name).get_options() + + +def register_conf_options(conf, group): + """Register the oslo.config options that are needed for a plugin. + + This only registers the basic options shared by all plugins. Options that + are specific to a plugin are loaded just before they are read. + + The defined options are: + + - auth_plugin: the name of the auth plugin that will be used for + authentication. + - auth_section: the group from which further auth plugin options should be + taken. If section is not provided then the auth plugin options will be + taken from the same group as provided in the parameters. + + :param oslo.config.Cfg conf: config object to register with. + :param string group: The ini group to register options in. + """ + conf.register_opt(_AUTH_SECTION_OPT, group=group) + + # NOTE(jamielennox): plugins are allowed to specify a 'section' which is + # the group that auth options should be taken from. If not present they + # come from the same as the base options were registered in. If present + # then the auth_plugin option may be read from that section so add that + # option. + if conf[group].auth_section: + group = conf[group].auth_section + + conf.register_opt(_AUTH_PLUGIN_OPT, group=group) + + +def load_from_conf_options(conf, group, **kwargs): + """Load a plugin from an oslo.config CONF object. + + Each plugin will register there own required options and so there is no + standard list and the plugin should be consulted. + + The base options should have been registered with register_conf_options + before this function is called. + + :param conf: An oslo.config conf object. + :param string group: The group name that options should be read from. + + :returns plugin: An authentication Plugin. + + :raises exceptions.NoMatchingPlugin: if a plugin cannot be created. + """ + # NOTE(jamielennox): plugins are allowed to specify a 'section' which is + # the group that auth options should be taken from. If not present they + # come from the same as the base options were registered in. + if conf[group].auth_section: + group = conf[group].auth_section + + name = conf[group].auth_plugin + if not name: + raise exceptions.NoMatchingPlugin('No plugin name provided for config') + + plugin_class = base.get_plugin_class(name) + plugin_opts = plugin_class.get_options() + conf.register_opts(plugin_opts, group=group) + + for opt in plugin_opts: + val = conf[group][opt.dest] + if val is not None: + val = opt.type(val) + kwargs.setdefault(opt.dest, val) + + return plugin_class.load_from_options(**kwargs) diff --git a/keystoneclient/auth/identity/base.py b/keystoneclient/auth/identity/base.py index 1f6a3c8..752ba37 100644 --- a/keystoneclient/auth/identity/base.py +++ b/keystoneclient/auth/identity/base.py @@ -13,6 +13,7 @@ import abc import logging +from oslo.config import cfg import six from keystoneclient import _discover @@ -178,3 +179,13 @@ class BaseIdentityPlugin(base.BaseAuthPlugin): session_endpoint_cache[sc_url] = disc return disc.url_for(version) + + @classmethod + def get_options(cls): + options = super(BaseIdentityPlugin, cls).get_options() + + options.extend([ + cfg.StrOpt('auth-url', help='Authentication URL'), + ]) + + return options diff --git a/keystoneclient/auth/identity/v2.py b/keystoneclient/auth/identity/v2.py index a421c67..11fbc39 100644 --- a/keystoneclient/auth/identity/v2.py +++ b/keystoneclient/auth/identity/v2.py @@ -12,6 +12,7 @@ import abc +from oslo.config import cfg import six from keystoneclient import access @@ -23,6 +24,18 @@ from keystoneclient import utils @six.add_metaclass(abc.ABCMeta) class Auth(base.BaseIdentityPlugin): + @classmethod + def get_options(cls): + options = super(Auth, cls).get_options() + + options.extend([ + cfg.StrOpt('tenant-id', help='Tenant ID'), + cfg.StrOpt('tenant-name', help='Tenant Name'), + cfg.StrOpt('trust-id', help='Trust ID'), + ]) + + return options + @utils.positional() def __init__(self, auth_url, trust_id=None, @@ -90,6 +103,20 @@ class Password(Auth): return {'passwordCredentials': {'username': self.username, 'password': self.password}} + @classmethod + def get_options(cls): + options = super(Password, cls).get_options() + + options.extend([ + cfg.StrOpt('user-name', + dest='username', + deprecated_name='username', + help='Username to login with'), + cfg.StrOpt('password', help='Password to use'), + ]) + + return options + class Token(Auth): @@ -106,3 +133,13 @@ class Token(Auth): if headers is not None: headers['X-Auth-Token'] = self.token return {'token': {'id': self.token}} + + @classmethod + def get_options(cls): + options = super(Token, cls).get_options() + + options.extend([ + cfg.StrOpt('token', help='Token'), + ]) + + return options diff --git a/keystoneclient/auth/identity/v3.py b/keystoneclient/auth/identity/v3.py index a169a17..63a0230 100644 --- a/keystoneclient/auth/identity/v3.py +++ b/keystoneclient/auth/identity/v3.py @@ -13,6 +13,7 @@ import abc import logging +from oslo.config import cfg import six from keystoneclient import access @@ -115,6 +116,24 @@ class Auth(base.BaseIdentityPlugin): return access.AccessInfoV3(resp.headers['X-Subject-Token'], **resp_data) + @classmethod + def get_options(cls): + options = super(Auth, cls).get_options() + + options.extend([ + cfg.StrOpt('domain-id', help='Domain ID to scope to'), + cfg.StrOpt('domain-name', help='Domain name to scope to'), + cfg.StrOpt('project-id', help='Project ID to scope to'), + cfg.StrOpt('project-name', help='Project name to scope to'), + cfg.StrOpt('project-domain-id', + help='Domain ID containing project'), + cfg.StrOpt('project-domain-name', + help='Domain name containing project'), + cfg.StrOpt('trust-id', help='Trust ID'), + ]) + + return options + @six.add_metaclass(abc.ABCMeta) class AuthMethod(object): @@ -214,6 +233,21 @@ class PasswordMethod(AuthMethod): class Password(AuthConstructor): _auth_method_class = PasswordMethod + @classmethod + def get_options(cls): + options = super(Password, cls).get_options() + + options.extend([ + cfg.StrOpt('user-id', help='User ID'), + cfg.StrOpt('user-name', dest='username', help='Username', + deprecated_name='username'), + cfg.StrOpt('user-domain-id', help="User's domain id"), + cfg.StrOpt('user-domain-name', help="User's domain name"), + cfg.StrOpt('password', help="User's password"), + ]) + + return options + class TokenMethod(AuthMethod): @@ -236,3 +270,13 @@ class Token(AuthConstructor): def __init__(self, auth_url, token, **kwargs): super(Token, self).__init__(auth_url, token=token, **kwargs) + + @classmethod + def get_options(cls): + options = super(Token, cls).get_options() + + options.extend([ + cfg.StrOpt('token', help='Token to authenticate with'), + ]) + + return options diff --git a/keystoneclient/auth/token_endpoint.py b/keystoneclient/auth/token_endpoint.py index 2245af7..9c22d44 100644 --- a/keystoneclient/auth/token_endpoint.py +++ b/keystoneclient/auth/token_endpoint.py @@ -37,3 +37,14 @@ class Token(base.BaseAuthPlugin): parameters passed to the plugin. """ return self.endpoint + + def get_options(self): + options = super(Token, self).get_options() + + options.extend([ + cfg.StrOpt('endpoint', + help='The endpoint that will always be used'), + cfg.StrOpt('token', help='The token that will always be used'), + ]) + + return options diff --git a/keystoneclient/tests/auth/test_conf.py b/keystoneclient/tests/auth/test_conf.py new file mode 100644 index 0000000..60828b8 --- /dev/null +++ b/keystoneclient/tests/auth/test_conf.py @@ -0,0 +1,176 @@ +# 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 uuid + +import mock +from oslo.config import cfg +import stevedore + +from keystoneclient.auth import base +from keystoneclient.auth import conf +from keystoneclient.auth.identity import v2 as v2_auth +from keystoneclient.auth.identity import v3 as v3_auth +from keystoneclient import exceptions +from keystoneclient.openstack.common.fixture import config +from keystoneclient.tests.auth import utils + + +class ConfTests(utils.TestCase): + + def setUp(self): + super(ConfTests, self).setUp() + self.conf_fixture = self.useFixture(config.Config()) + + # NOTE(jamielennox): we register the basic config options first because + # we need them in place before we can stub them. We will need to run + # the register again after we stub the auth section and auth plugin so + # it can load the plugin specific options. + conf.register_conf_options(self.conf_fixture.conf, group=self.GROUP) + + def test_loading_v2(self): + section = uuid.uuid4().hex + username = uuid.uuid4().hex + password = uuid.uuid4().hex + trust_id = uuid.uuid4().hex + tenant_id = uuid.uuid4().hex + + self.conf_fixture.config(auth_section=section, group=self.GROUP) + conf.register_conf_options(self.conf_fixture.conf, group=self.GROUP) + + self.conf_fixture.register_opts(v2_auth.Password.get_options(), + group=section) + + self.conf_fixture.config(auth_plugin=self.V2PASS, + username=username, + password=password, + trust_id=trust_id, + tenant_id=tenant_id, + group=section) + + a = conf.load_from_conf_options(self.conf_fixture.conf, self.GROUP) + + self.assertEqual(username, a.username) + self.assertEqual(password, a.password) + self.assertEqual(trust_id, a.trust_id) + self.assertEqual(tenant_id, a.tenant_id) + + def test_loading_v3(self): + section = uuid.uuid4().hex + token = uuid.uuid4().hex + trust_id = uuid.uuid4().hex + project_id = uuid.uuid4().hex + project_domain_name = uuid.uuid4().hex + + self.conf_fixture.config(auth_section=section, group=self.GROUP) + conf.register_conf_options(self.conf_fixture.conf, group=self.GROUP) + + self.conf_fixture.register_opts(v3_auth.Token.get_options(), + group=section) + + self.conf_fixture.config(auth_plugin=self.V3TOKEN, + token=token, + trust_id=trust_id, + project_id=project_id, + project_domain_name=project_domain_name, + group=section) + + a = conf.load_from_conf_options(self.conf_fixture.conf, self.GROUP) + + self.assertEqual(token, a.auth_methods[0].token) + self.assertEqual(trust_id, a.trust_id) + self.assertEqual(project_id, a.project_id) + self.assertEqual(project_domain_name, a.project_domain_name) + + def test_loading_invalid_plugin(self): + self.conf_fixture.config(auth_plugin=uuid.uuid4().hex, + group=self.GROUP) + + self.assertRaises(exceptions.NoMatchingPlugin, + conf.load_from_conf_options, + self.conf_fixture.conf, + self.GROUP) + + def test_loading_with_no_data(self): + self.assertRaises(exceptions.NoMatchingPlugin, + conf.load_from_conf_options, + self.conf_fixture.conf, + self.GROUP) + + @mock.patch('stevedore.DriverManager') + def test_other_params(self, m): + m.return_value = utils.MockManager(utils.MockPlugin) + driver_name = uuid.uuid4().hex + + self.conf_fixture.register_opts(utils.MockPlugin.get_options(), + group=self.GROUP) + self.conf_fixture.config(auth_plugin=driver_name, + group=self.GROUP, + **self.TEST_VALS) + + a = conf.load_from_conf_options(self.conf_fixture.conf, self.GROUP) + self.assertTestVals(a) + + m.assert_called_once_with(namespace=base.PLUGIN_NAMESPACE, + name=driver_name, + invoke_on_load=False) + + @utils.mock_plugin + def test_same_section(self, m): + self.conf_fixture.register_opts(utils.MockPlugin.get_options(), + group=self.GROUP) + conf.register_conf_options(self.conf_fixture.conf, group=self.GROUP) + self.conf_fixture.config(auth_plugin=uuid.uuid4().hex, + group=self.GROUP, + **self.TEST_VALS) + + a = conf.load_from_conf_options(self.conf_fixture.conf, self.GROUP) + self.assertTestVals(a) + + @utils.mock_plugin + def test_diff_section(self, m): + section = uuid.uuid4().hex + + self.conf_fixture.config(auth_section=section, group=self.GROUP) + conf.register_conf_options(self.conf_fixture.conf, group=self.GROUP) + + self.conf_fixture.register_opts(utils.MockPlugin.get_options(), + group=section) + self.conf_fixture.config(group=section, + auth_plugin=uuid.uuid4().hex, + **self.TEST_VALS) + + a = conf.load_from_conf_options(self.conf_fixture.conf, self.GROUP) + self.assertTestVals(a) + + def test_plugins_are_all_opts(self): + manager = stevedore.ExtensionManager(base.PLUGIN_NAMESPACE, + invoke_on_load=False, + propagate_map_exceptions=True) + + def inner(driver): + for p in driver.plugin.get_options(): + self.assertIsInstance(p, cfg.Opt) + + manager.map(inner) + + def test_get_common(self): + opts = conf.get_common_conf_options() + for opt in opts: + self.assertIsInstance(opt, cfg.Opt) + self.assertEqual(2, len(opts)) + + def test_get_named(self): + loaded_opts = conf.get_plugin_options('v2password') + plugin_opts = v2_auth.Password.get_options() + + self.assertEqual(plugin_opts, loaded_opts) diff --git a/keystoneclient/tests/auth/utils.py b/keystoneclient/tests/auth/utils.py new file mode 100644 index 0000000..5cc7011 --- /dev/null +++ b/keystoneclient/tests/auth/utils.py @@ -0,0 +1,83 @@ +# 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 functools + +import mock +from oslo.config import cfg +import six + +from keystoneclient.auth import base +from keystoneclient.tests import utils + + +class MockPlugin(base.BaseAuthPlugin): + + INT_DESC = 'test int' + FLOAT_DESC = 'test float' + BOOL_DESC = 'test bool' + + def __init__(self, **kwargs): + self._data = kwargs + + def __getitem__(self, key): + return self._data[key] + + def get_token(self, *args, **kwargs): + return 'aToken' + + def get_endpoint(self, *args, **kwargs): + return 'http://test' + + @classmethod + def get_options(cls): + return [ + cfg.IntOpt('a-int', default='3', help=cls.INT_DESC), + cfg.BoolOpt('a-bool', help=cls.BOOL_DESC), + cfg.FloatOpt('a-float', help=cls.FLOAT_DESC), + ] + + +class MockManager(object): + + def __init__(self, driver): + self.driver = driver + + +def mock_plugin(f): + @functools.wraps(f) + def inner(*args, **kwargs): + with mock.patch.object(base, 'get_plugin_class') as m: + m.return_value = MockPlugin + args = list(args) + [m] + return f(*args, **kwargs) + + return inner + + +class TestCase(utils.TestCase): + + GROUP = 'auth' + V2PASS = 'v2password' + V3TOKEN = 'v3token' + + a_int = 88 + a_float = 88.8 + a_bool = False + + TEST_VALS = {'a_int': a_int, + 'a_float': a_float, + 'a_bool': a_bool} + + def assertTestVals(self, plugin, vals=TEST_VALS): + for k, v in six.iteritems(vals): + self.assertEqual(v, plugin[k]) diff --git a/requirements.txt b/requirements.txt index d1159c2..1dde3a6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,3 +7,4 @@ pbr>=0.6,!=0.7,<1.0 PrettyTable>=0.7,<0.8 requests>=1.1 six>=1.7.0 +stevedore>=0.14 @@ -31,6 +31,12 @@ setup-hooks = console_scripts = keystone = keystoneclient.shell:main +keystoneclient.auth.plugin = + v2password = keystoneclient.auth.identity.v2:Password + v2token = keystoneclient.auth.identity.v2:Token + v3password = keystoneclient.auth.identity.v3:Password + v3token = keystoneclient.auth.identity.v3:Token + [build_sphinx] source-dir = doc/source build-dir = doc/build diff --git a/test-requirements.txt b/test-requirements.txt index 75c049e..0d1aefc 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -9,7 +9,6 @@ mox3>=0.7.0 oauthlib>=0.6 pycrypto>=2.6 sphinx>=1.1.2,!=1.2.0,<1.3 -stevedore>=0.14 testrepository>=0.0.18 testresources>=0.2.4 testtools>=0.9.34 |