summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJamie Lennox <jamielennox@redhat.com>2014-03-11 15:59:10 +1000
committerJamie Lennox <jamielennox@redhat.com>2014-07-07 14:15:10 +1000
commit5c91ede44768ebbb2fff12f9a7c93e63b9bbd56d (patch)
tree66943ba183ba7dfd770b26c6abe5ab30dca970ab
parent3e88c35cd72190c2529c9ccaaf38961a33f30744 (diff)
downloadpython-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.py46
-rw-r--r--keystoneclient/auth/conf.py116
-rw-r--r--keystoneclient/auth/identity/base.py11
-rw-r--r--keystoneclient/auth/identity/v2.py37
-rw-r--r--keystoneclient/auth/identity/v3.py44
-rw-r--r--keystoneclient/auth/token_endpoint.py11
-rw-r--r--keystoneclient/tests/auth/test_conf.py176
-rw-r--r--keystoneclient/tests/auth/utils.py83
-rw-r--r--requirements.txt1
-rw-r--r--setup.cfg6
-rw-r--r--test-requirements.txt1
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
diff --git a/setup.cfg b/setup.cfg
index ee649b8..3e55932 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -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