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
This commit is contained in:
Jamie Lennox 2014-03-11 15:59:10 +10:00
parent 3e88c35cd7
commit 5c91ede447
11 changed files with 531 additions and 1 deletions

View File

@ -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)

116
keystoneclient/auth/conf.py Normal file
View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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])

View File

@ -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

View File

@ -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

View File

@ -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