Merge "Improve configuration of out-of-tree identity drivers"
This commit is contained in:
commit
1b78b57ec5
@ -74,6 +74,62 @@ The TLDR; steps (and too long didn't write yet):
|
|||||||
5. Configure your new driver in ``keystone.conf``
|
5. Configure your new driver in ``keystone.conf``
|
||||||
6. Sit back and enjoy!
|
6. Sit back and enjoy!
|
||||||
|
|
||||||
|
Identity Driver Configuration
|
||||||
|
-----------------------------
|
||||||
|
|
||||||
|
As described in the :ref:`domain_specific_configuration` there are 2 ways of
|
||||||
|
configuring domain specific drivers: using files and using database.
|
||||||
|
Configuration with files is straight forward but is having a major disadvantage
|
||||||
|
of requiring restart of Keystone for the refresh of configuration or even for
|
||||||
|
Keystone to start using chosen driver after adding a new domain.
|
||||||
|
|
||||||
|
Configuring drivers using database is a flexible alternative that allows
|
||||||
|
dynamic reconfiguration and even changes using the API (requires admin
|
||||||
|
privileges by default). There are 2 independent parts for this to work properly:
|
||||||
|
|
||||||
|
Defining configuration options
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Driver class (as pointed by EntryPoints) may have a static method
|
||||||
|
`register_opts` accepting `conf` argument. This method, if present, is being
|
||||||
|
invoked during loading the driver and registered options are then
|
||||||
|
available when the driver is being instantiated.
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
class CustomDriver(base.IdentityDriverBase):
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def register_opts(cls, conf):
|
||||||
|
grp = cfg.OptGroup("foo")
|
||||||
|
opts = [cfg.StrOpt("opt1")]
|
||||||
|
conf.register_group(grp)
|
||||||
|
conf.register_opts(opts, group=grp)
|
||||||
|
|
||||||
|
def __init__(self, conf=None):
|
||||||
|
# conf contains options registered above and domain specific values
|
||||||
|
# being set.
|
||||||
|
pass
|
||||||
|
|
||||||
|
...
|
||||||
|
|
||||||
|
Allowing domain configuration per API
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
A safety measure of the Keystone domain configuration API is that options
|
||||||
|
allowed for the change need to be explicitly whitelisted. This is done
|
||||||
|
in the `domain_config` section of the main Keystone configuration file.
|
||||||
|
|
||||||
|
.. code-block:: cfg
|
||||||
|
|
||||||
|
[domain_config]
|
||||||
|
additional_whitelisted_options=<GROUP_NAME>:[opt1,opt2,opt3]
|
||||||
|
additional_sensitive_options=<GROUP_NAME>:[password]
|
||||||
|
|
||||||
|
The `<GROUP_NAME>` is the name of the configuration group as defined by the
|
||||||
|
driver. Sensitive options are not included in the GET api call and are stored
|
||||||
|
in a separate database table.
|
||||||
|
|
||||||
Driver Interface Changes
|
Driver Interface Changes
|
||||||
------------------------
|
------------------------
|
||||||
|
|
||||||
|
@ -42,12 +42,32 @@ Time-to-live (TTL, in seconds) to cache domain-specific configuration data.
|
|||||||
This has no effect unless `[domain_config] caching` is enabled.
|
This has no effect unless `[domain_config] caching` is enabled.
|
||||||
"""))
|
"""))
|
||||||
|
|
||||||
|
additional_whitelisted_options = cfg.Opt(
|
||||||
|
'additional_whitelisted_options',
|
||||||
|
type=cfg.types.Dict(value_type=cfg.types.List(bounds=True)),
|
||||||
|
help=utils.fmt("""
|
||||||
|
Additional whitelisted domain-specific options for out-of-tree drivers.
|
||||||
|
This is a dictonary of lists with the key being the group name and value a list
|
||||||
|
of group options.""")
|
||||||
|
)
|
||||||
|
|
||||||
|
additional_sensitive_options = cfg.Opt(
|
||||||
|
'additional_sensitive_options',
|
||||||
|
type=cfg.types.Dict(value_type=cfg.types.List(bounds=True)),
|
||||||
|
help=utils.fmt("""
|
||||||
|
Additional sensitive domain-specific options for out-of-tree drivers.
|
||||||
|
This is a dictonary of lists with the key being the group name and value a list
|
||||||
|
of group options.""")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
GROUP_NAME = __name__.split('.')[-1]
|
GROUP_NAME = __name__.split('.')[-1]
|
||||||
ALL_OPTS = [
|
ALL_OPTS = [
|
||||||
driver,
|
driver,
|
||||||
caching,
|
caching,
|
||||||
cache_time,
|
cache_time,
|
||||||
|
additional_whitelisted_options,
|
||||||
|
additional_sensitive_options
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -134,6 +134,16 @@ class IdentityDriverBase(object, metaclass=abc.ABCMeta):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# @classmethod
|
||||||
|
# def register_opts(cls, conf):
|
||||||
|
# """Register driver specific configuration options.
|
||||||
|
|
||||||
|
# For domain configuration being stored in the database it is necessary
|
||||||
|
# for the driver to register configuration options. This method is
|
||||||
|
# optional and if it is not present no options are registered.
|
||||||
|
# """
|
||||||
|
# pass
|
||||||
|
|
||||||
def _get_conf(self):
|
def _get_conf(self):
|
||||||
try:
|
try:
|
||||||
return self.conf or CONF
|
return self.conf or CONF
|
||||||
|
@ -25,6 +25,7 @@ import uuid
|
|||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
from oslo_log import log
|
from oslo_log import log
|
||||||
from pycadf import reason
|
from pycadf import reason
|
||||||
|
import stevedore
|
||||||
|
|
||||||
from keystone import assignment # TODO(lbragstad): Decouple this dependency
|
from keystone import assignment # TODO(lbragstad): Decouple this dependency
|
||||||
from keystone.common import cache
|
from keystone.common import cache
|
||||||
@ -64,6 +65,23 @@ REGISTRATION_ATTEMPTS = 10
|
|||||||
SQL_DRIVER = 'SQL'
|
SQL_DRIVER = 'SQL'
|
||||||
|
|
||||||
|
|
||||||
|
def get_driver(namespace, driver_name, *args):
|
||||||
|
"""Get identity driver without initializing.
|
||||||
|
|
||||||
|
The method is invoked to be able to introspect domain specific driver
|
||||||
|
looking for additional configuration options required by the driver.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
driver_manager = stevedore.DriverManager(namespace,
|
||||||
|
driver_name,
|
||||||
|
invoke_on_load=False,
|
||||||
|
invoke_args=args)
|
||||||
|
return driver_manager.driver
|
||||||
|
except stevedore.exception.NoMatches:
|
||||||
|
msg = (_('Unable to find %(name)r driver in %(namespace)r.'))
|
||||||
|
raise ImportError(msg % {'name': driver_name, 'namespace': namespace})
|
||||||
|
|
||||||
|
|
||||||
class DomainConfigs(provider_api.ProviderAPIMixin, dict):
|
class DomainConfigs(provider_api.ProviderAPIMixin, dict):
|
||||||
"""Discover, store and provide access to domain specific configs.
|
"""Discover, store and provide access to domain specific configs.
|
||||||
|
|
||||||
@ -262,12 +280,51 @@ class DomainConfigs(provider_api.ProviderAPIMixin, dict):
|
|||||||
default_config_files=[],
|
default_config_files=[],
|
||||||
default_config_dirs=[])
|
default_config_dirs=[])
|
||||||
|
|
||||||
|
# Try to identify the required driver for the domain to let it register
|
||||||
|
# supported configuration options. In difference to the FS based
|
||||||
|
# configuration this is being set through `oslo_cfg.set_override` and
|
||||||
|
# thus require special treatment.
|
||||||
|
try:
|
||||||
|
driver_name = specific_config.get("identity", {}).get(
|
||||||
|
"driver", domain_config["cfg"].identity.driver)
|
||||||
|
# For the non in-tree drivers ...
|
||||||
|
if driver_name not in ["sql", "ldap"]:
|
||||||
|
# Locate the driver without invoking ...
|
||||||
|
driver = get_driver(
|
||||||
|
Manager.driver_namespace, driver_name)
|
||||||
|
# Check whether it wants to register additional config options
|
||||||
|
# ...
|
||||||
|
if hasattr(driver, "register_opts"):
|
||||||
|
# And register them for the domain_config (not the global
|
||||||
|
# Keystone config)
|
||||||
|
driver.register_opts(domain_config["cfg"])
|
||||||
|
except Exception as ex:
|
||||||
|
# If we failed for some reason - something wrong with the driver,
|
||||||
|
# so let's just skip registering config options. This matches older
|
||||||
|
# behavior of Keystone where out-of-tree drivers were not able to
|
||||||
|
# register config options with the DB configuration loading branch.
|
||||||
|
LOG.debug(
|
||||||
|
f"Exception during attempt to load domain specific "
|
||||||
|
f"configuration options: {ex}")
|
||||||
|
|
||||||
# Override any options that have been passed in as specified in the
|
# Override any options that have been passed in as specified in the
|
||||||
# database.
|
# database.
|
||||||
for group in specific_config:
|
for group in specific_config:
|
||||||
for option in specific_config[group]:
|
for option in specific_config[group]:
|
||||||
domain_config['cfg'].set_override(
|
# NOTE(gtema): Very first time default driver is being ordered
|
||||||
option, specific_config[group][option], group)
|
# to process the domain. This will change once initialization
|
||||||
|
# completes. Until the driver specific configuration is being
|
||||||
|
# registered `set_override` will fail for options not known by
|
||||||
|
# the core Keystone. Make this loading not failing letting code
|
||||||
|
# to complete the process properly.
|
||||||
|
try:
|
||||||
|
domain_config['cfg'].set_override(
|
||||||
|
option, specific_config[group][option], group)
|
||||||
|
except (cfg.NoSuchOptError, cfg.NoSuchGroupError):
|
||||||
|
# Error to register config overrides for wrong driver. This
|
||||||
|
# is not worth of logging since it is a normal case during
|
||||||
|
# Keystone initialization.
|
||||||
|
pass
|
||||||
|
|
||||||
domain_config['cfg_overrides'] = specific_config
|
domain_config['cfg_overrides'] = specific_config
|
||||||
domain_config['driver'] = self._load_driver(domain_config)
|
domain_config['driver'] = self._load_driver(domain_config)
|
||||||
|
@ -1124,6 +1124,13 @@ class DomainConfigManager(manager.Manager):
|
|||||||
'option': option}
|
'option': option}
|
||||||
raise exception.UnexpectedError(exception=msg)
|
raise exception.UnexpectedError(exception=msg)
|
||||||
|
|
||||||
|
if CONF.domain_config.additional_whitelisted_options:
|
||||||
|
self.whitelisted_options.update(
|
||||||
|
**CONF.domain_config.additional_whitelisted_options)
|
||||||
|
if CONF.domain_config.additional_sensitive_options:
|
||||||
|
self.sensitive_options.update(
|
||||||
|
**CONF.domain_config.additional_sensitive_options)
|
||||||
|
|
||||||
if (group and group not in self.whitelisted_options and
|
if (group and group not in self.whitelisted_options and
|
||||||
group not in self.sensitive_options):
|
group not in self.sensitive_options):
|
||||||
msg = _('Group %(group)s is not supported '
|
msg = _('Group %(group)s is not supported '
|
||||||
@ -1131,15 +1138,15 @@ class DomainConfigManager(manager.Manager):
|
|||||||
raise exception.InvalidDomainConfig(reason=msg)
|
raise exception.InvalidDomainConfig(reason=msg)
|
||||||
|
|
||||||
if option:
|
if option:
|
||||||
if (option not in self.whitelisted_options[group] and option not in
|
if (option not in self.whitelisted_options.get(group, {})
|
||||||
self.sensitive_options[group]):
|
and option not in self.sensitive_options.get(group, {})):
|
||||||
msg = _('Option %(option)s in group %(group)s is not '
|
msg = _('Option %(option)s in group %(group)s is not '
|
||||||
'supported for domain specific configurations') % {
|
'supported for domain specific configurations') % {
|
||||||
'group': group, 'option': option}
|
'group': group, 'option': option}
|
||||||
raise exception.InvalidDomainConfig(reason=msg)
|
raise exception.InvalidDomainConfig(reason=msg)
|
||||||
|
|
||||||
def _is_sensitive(self, group, option):
|
def _is_sensitive(self, group, option):
|
||||||
return option in self.sensitive_options[group]
|
return option in self.sensitive_options.get(group, {})
|
||||||
|
|
||||||
def _config_to_list(self, config):
|
def _config_to_list(self, config):
|
||||||
"""Build list of options for use by backend drivers."""
|
"""Build list of options for use by backend drivers."""
|
||||||
|
94
keystone/tests/unit/identity/backends/fake_driver.py
Normal file
94
keystone/tests/unit/identity/backends/fake_driver.py
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
"""Fake driver to test out-of-tree drivers handling."""
|
||||||
|
|
||||||
|
from oslo_config import cfg
|
||||||
|
|
||||||
|
from keystone import exception
|
||||||
|
from keystone.identity.backends import base
|
||||||
|
|
||||||
|
|
||||||
|
class FooDriver(base.IdentityDriverBase):
|
||||||
|
"""Fake out-of-tree driver.
|
||||||
|
|
||||||
|
It does not make much sense to inherit from BaseClass, but in certain
|
||||||
|
places across the code methods are invoked
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def register_opts(cls, conf):
|
||||||
|
grp = cfg.OptGroup("foo")
|
||||||
|
opts = [cfg.StrOpt("opt1")]
|
||||||
|
conf.register_group(grp)
|
||||||
|
conf.register_opts(opts, group=grp)
|
||||||
|
|
||||||
|
def authenticate(self, user_id, password):
|
||||||
|
raise exception.NotImplemented() # pragma: no cover
|
||||||
|
|
||||||
|
def create_user(self, user_id, user):
|
||||||
|
raise exception.NotImplemented() # pragma: no cover
|
||||||
|
|
||||||
|
def list_users(self, hints):
|
||||||
|
raise exception.NotImplemented() # pragma: no cover
|
||||||
|
|
||||||
|
def unset_default_project_id(self, project_id):
|
||||||
|
raise exception.NotImplemented() # pragma: no cover
|
||||||
|
|
||||||
|
def list_users_in_group(self, group_id, hints):
|
||||||
|
raise exception.NotImplemented() # pragma: no cover
|
||||||
|
|
||||||
|
def get_user(self, user_id):
|
||||||
|
raise exception.NotImplemented() # pragma: no cover
|
||||||
|
|
||||||
|
def update_user(self, user_id, user):
|
||||||
|
raise exception.NotImplemented() # pragma: no cover
|
||||||
|
|
||||||
|
def change_password(self, user_id, new_password):
|
||||||
|
raise exception.NotImplemented() # pragma: no cover
|
||||||
|
|
||||||
|
def add_user_to_group(self, user_id, group_id):
|
||||||
|
raise exception.NotImplemented() # pragma: no cover
|
||||||
|
|
||||||
|
def check_user_in_group(self, user_id, group_id):
|
||||||
|
raise exception.NotImplemented() # pragma: no cover
|
||||||
|
|
||||||
|
def remove_user_from_group(self, user_id, group_id):
|
||||||
|
raise exception.NotImplemented() # pragma: no cover
|
||||||
|
|
||||||
|
def delete_user(self, user_id):
|
||||||
|
raise exception.NotImplemented() # pragma: no cover
|
||||||
|
|
||||||
|
def get_user_by_name(self, user_name, domain_id):
|
||||||
|
raise exception.NotImplemented() # pragma: no cover
|
||||||
|
|
||||||
|
def create_group(self, group_id, group):
|
||||||
|
raise exception.NotImplemented() # pragma: no cover
|
||||||
|
|
||||||
|
def list_groups(self, hints):
|
||||||
|
raise exception.NotImplemented() # pragma: no cover
|
||||||
|
|
||||||
|
def list_groups_for_user(self, user_id, hints):
|
||||||
|
raise exception.NotImplemented() # pragma: no cover
|
||||||
|
|
||||||
|
def get_group(self, group_id):
|
||||||
|
raise exception.NotImplemented() # pragma: no cover
|
||||||
|
|
||||||
|
def get_group_by_name(self, group_name, domain_id):
|
||||||
|
raise exception.NotImplemented() # pragma: no cover
|
||||||
|
|
||||||
|
def update_group(self, group_id, group):
|
||||||
|
raise exception.NotImplemented() # pragma: no cover
|
||||||
|
|
||||||
|
def delete_group(self, group_id):
|
||||||
|
raise exception.NotImplemented() # pragma: no cover
|
@ -12,12 +12,14 @@
|
|||||||
|
|
||||||
"""Unit tests for core identity behavior."""
|
"""Unit tests for core identity behavior."""
|
||||||
|
|
||||||
|
import fixtures
|
||||||
import itertools
|
import itertools
|
||||||
import os
|
import os
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
from oslo_config import fixture as config_fixture
|
from oslo_config import fixture as config_fixture
|
||||||
|
import stevedore
|
||||||
|
|
||||||
from keystone.common import provider_api
|
from keystone.common import provider_api
|
||||||
import keystone.conf
|
import keystone.conf
|
||||||
@ -25,6 +27,7 @@ from keystone import exception
|
|||||||
from keystone import identity
|
from keystone import identity
|
||||||
from keystone.tests import unit
|
from keystone.tests import unit
|
||||||
from keystone.tests.unit import default_fixtures
|
from keystone.tests.unit import default_fixtures
|
||||||
|
from keystone.tests.unit.identity.backends import fake_driver
|
||||||
from keystone.tests.unit.ksfixtures import database
|
from keystone.tests.unit.ksfixtures import database
|
||||||
|
|
||||||
|
|
||||||
@ -181,3 +184,50 @@ class TestDatabaseDomainConfigs(unit.TestCase):
|
|||||||
self.assertEqual(CONF.ldap.suffix, res.ldap.suffix)
|
self.assertEqual(CONF.ldap.suffix, res.ldap.suffix)
|
||||||
self.assertEqual(CONF.ldap.use_tls, res.ldap.use_tls)
|
self.assertEqual(CONF.ldap.use_tls, res.ldap.use_tls)
|
||||||
self.assertEqual(CONF.ldap.query_scope, res.ldap.query_scope)
|
self.assertEqual(CONF.ldap.query_scope, res.ldap.query_scope)
|
||||||
|
|
||||||
|
def test_loading_config_from_database_out_of_tree(self):
|
||||||
|
# Test domain config loading for out-of-tree driver supporting own
|
||||||
|
# config options
|
||||||
|
|
||||||
|
# Prepare fake driver
|
||||||
|
extension = stevedore.extension.Extension(
|
||||||
|
name="foo", entry_point=None,
|
||||||
|
obj=fake_driver.FooDriver(), plugin=None
|
||||||
|
)
|
||||||
|
fake_driver_manager = stevedore.DriverManager.make_test_instance(
|
||||||
|
extension, namespace="keystone.identity"
|
||||||
|
)
|
||||||
|
# replace DriverManager with a patched test instance
|
||||||
|
self.useFixture(
|
||||||
|
fixtures.MockPatchObject(
|
||||||
|
stevedore, "DriverManager", return_value=fake_driver_manager
|
||||||
|
)
|
||||||
|
).mock
|
||||||
|
|
||||||
|
self.config_fixture.config(
|
||||||
|
domain_configurations_from_database=True, group="identity"
|
||||||
|
)
|
||||||
|
self.config_fixture.config(
|
||||||
|
additional_whitelisted_options={"foo": ["opt1"]},
|
||||||
|
group="domain_config"
|
||||||
|
)
|
||||||
|
domain = unit.new_domain_ref()
|
||||||
|
PROVIDERS.resource_api.create_domain(domain["id"], domain)
|
||||||
|
# Override two config options for our domain
|
||||||
|
conf = {
|
||||||
|
"foo": {"opt1": uuid.uuid4().hex},
|
||||||
|
"identity": {"driver": "foo"}}
|
||||||
|
PROVIDERS.domain_config_api.create_config(domain["id"], conf)
|
||||||
|
domain_config = identity.DomainConfigs()
|
||||||
|
domain_config.setup_domain_drivers("foo", PROVIDERS.resource_api)
|
||||||
|
# Make sure our two overrides are in place, and others are not affected
|
||||||
|
res = domain_config.get_domain_conf(domain["id"])
|
||||||
|
|
||||||
|
self.assertEqual(conf["foo"]["opt1"], res.foo.opt1)
|
||||||
|
|
||||||
|
# Reset whitelisted options in the provider directly. Due to the fact
|
||||||
|
# that there are too many singletons used around the code basis there
|
||||||
|
# is a chance of clash when other API domain_config tests are being
|
||||||
|
# executed by the same process. It is NOT ENOUGH just to invoke reset
|
||||||
|
# on fixtures.
|
||||||
|
PROVIDERS.domain_config_api.whitelisted_options.pop("foo", None)
|
||||||
|
@ -0,0 +1,8 @@
|
|||||||
|
---
|
||||||
|
features:
|
||||||
|
- |
|
||||||
|
Improve configuration management for the out-of-tree identity drivers. When
|
||||||
|
driver implements a special method it is being invoked before instantiating
|
||||||
|
the driver when reading configuration from the database. Also 2 new
|
||||||
|
`domain_config` section configuration options are added to allow such
|
||||||
|
driver specific parameters to be managed using the API.
|
Loading…
Reference in New Issue
Block a user