Add Castellan Oslo Config Driver.

This driver is an oslo.config backend driver implemented with
Castellan. It extends oslo.config's capabilities by enabling it
to retrieve configuration values from a secret manager behind
Castellan.

Change-Id: Id7cf99bea5788e0a6309461a75eaa8d08d29641b
Signed-off-by: Moises Guimaraes de Medeiros <moguimar@redhat.com>
This commit is contained in:
Moises Guimaraes de Medeiros 2018-09-04 14:54:01 +02:00 committed by Moisés Guimarães de Medeiros
parent 0450c73819
commit 6e03a68c14
7 changed files with 256 additions and 2 deletions

1
.gitignore vendored
View File

@ -28,6 +28,7 @@ pip-log.txt
.stestr/
.venv
cover
vault_*
# Translations
*.mo

141
castellan/_config_driver.py Normal file
View File

@ -0,0 +1,141 @@
# 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.
r"""
Castellan Oslo Config Driver
----------------------------
This driver is an oslo.config backend driver implemented with Castellan. It
extends oslo.config's capabilities by enabling it to retrieve configuration
values from a secret manager behind Castellan.
The setup of a Castellan configuration source is as follow::
[DEFAULT]
config_source = castellan_config_group
[castellan_config_group]
driver = castellan
config_file = castellan.conf
mapping_file = mapping.conf
In the following sessions, you can find more information about this driver's
classes and its options.
The Driver Class
================
.. autoclass:: CastellanConfigurationSourceDriver
The Configuration Source Class
==============================
.. autoclass:: CastellanConfigurationSource
"""
from castellan.common.exception import KeyManagerError
from castellan.common.exception import ManagedObjectNotFoundError
from castellan import key_manager
from oslo_config import cfg
from oslo_config import sources
from oslo_log import log
LOG = log.getLogger(__name__)
class CastellanConfigurationSourceDriver(sources.ConfigurationSourceDriver):
"""A backend driver for configuration values served through castellan.
Required options:
- config_file: The castellan configuration file.
- mapping_file: A configuration/castellan_id mapping file. This file
creates connections between configuration options and
castellan ids. The group and option name remains the
same, while the value gets stored a secret manager behind
castellan and is replaced by its castellan id. The ids
will be used to fetch the values through castellan.
"""
_castellan_driver_opts = [
cfg.StrOpt(
'config_file',
required=True,
sample_default='etc/castellan/castellan.conf',
help=('The path to a castellan configuration file.'),
),
cfg.StrOpt(
'mapping_file',
required=True,
sample_default='etc/castellan/secrets_mapping.conf',
help=('The path to a configuration/castellan_id mapping file.'),
),
]
def list_options_for_discovery(self):
return self._castellan_driver_opts
def open_source_from_opt_group(self, conf, group_name):
conf.register_opts(self._castellan_driver_opts, group_name)
return CastellanConfigurationSource(
group_name,
conf[group_name].config_file,
conf[group_name].mapping_file)
class CastellanConfigurationSource(sources.ConfigurationSource):
"""A configuration source for configuration values served through castellan.
:param config_file: The path to a castellan configuration file.
:param mapping_file: The path to a configuration/castellan_id mapping file.
"""
def __init__(self, group_name, config_file, mapping_file):
conf = cfg.ConfigOpts()
conf(args=[], default_config_files=[config_file])
self._name = group_name
self._mngr = key_manager.API(conf)
self._mapping = {}
cfg.ConfigParser(mapping_file, self._mapping).parse()
def get(self, group_name, option_name, opt):
try:
group_name = group_name or "DEFAULT"
castellan_id = self._mapping[group_name][option_name][0]
return (self._mngr.get("ctx", castellan_id).get_encoded().decode(),
cfg.LocationInfo(cfg.Locations.user, castellan_id))
except KeyError:
# no mapping 'option = castellan_id'
LOG.info("option '[%s] %s' not present in '[%s] mapping_file'",
group_name, option_name, self._name)
except KeyManagerError:
# bad mapping 'option =' without a castellan_id
LOG.warning("missing castellan_id for option "
"'[%s] %s' in '[%s] mapping_file'",
group_name, option_name, self._name)
except ManagedObjectNotFoundError:
# good mapping, but unknown castellan_id by secret manager
LOG.warning("invalid castellan_id for option "
"'[%s] %s' in '[%s] mapping_file'",
group_name, option_name, self._name)
return (sources._NoValue, None)

View File

@ -0,0 +1,108 @@
# 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.
"""
Functional test cases for the Castellan Oslo Config Driver.
Note: This requires local running instance of Vault.
"""
import tempfile
from oslo_config import cfg
from oslo_config import fixture
from oslotest import base
from castellan import _config_driver
from castellan.common.objects import opaque_data
from castellan.tests.unit.key_manager import fake
class CastellanSourceTestCase(base.BaseTestCase):
def setUp(self):
super(CastellanSourceTestCase, self).setUp()
self.driver = _config_driver.CastellanConfigurationSourceDriver()
self.conf = cfg.ConfigOpts()
self.conf_fixture = self.useFixture(fixture.Config(self.conf))
def test_incomplete_driver(self):
# The group exists, but does not specify the
# required options for this driver.
self.conf_fixture.load_raw_values(
group='incomplete_driver',
driver='castellan',
)
source = self.conf._open_source_from_opt_group('incomplete_driver')
self.assertIsNone(source)
self.assertEqual(self.conf.incomplete_driver.driver, 'castellan')
def test_complete_driver(self):
self.conf_fixture.load_raw_values(
group='castellan_source',
driver='castellan',
config_file='config.conf',
mapping_file='mapping.conf',
)
with base.mock.patch.object(
_config_driver,
'CastellanConfigurationSource') as source_class:
self.driver.open_source_from_opt_group(
self.conf, 'castellan_source')
source_class.assert_called_once_with(
'castellan_source',
self.conf.castellan_source.config_file,
self.conf.castellan_source.mapping_file)
def test_fetch_secret(self):
# fake KeyManager populated with secret
km = fake.fake_api()
secret_id = km.store("fake_context",
opaque_data.OpaqueData(b"super_secret!"))
# driver config
config = "[key_manager]\nbackend=vault"
mapping = "[DEFAULT]\nmy_secret=" + secret_id
# creating temp files
with tempfile.NamedTemporaryFile() as config_file:
config_file.write(config.encode("utf-8"))
config_file.flush()
with tempfile.NamedTemporaryFile() as mapping_file:
mapping_file.write(mapping.encode("utf-8"))
mapping_file.flush()
self.conf_fixture.load_raw_values(
group='castellan_source',
driver='castellan',
config_file=config_file.name,
mapping_file=mapping_file.name,
)
source = self.driver.open_source_from_opt_group(
self.conf,
'castellan_source')
# replacing key_manager with fake one
source._mngr = km
# testing if the source is able to retrieve
# the secret value stored in the key_manager
# using the secret_id from the mapping file
self.assertEqual("super_secret!",
source.get("DEFAULT",
"my_secret",
cfg.StrOpt(""))[0])

View File

@ -33,7 +33,7 @@ netaddr==0.7.18
netifaces==0.10.4
openstackdocstheme==1.18.1
os-client-config==1.28.0
oslo.config==5.2.0
oslo.config==6.4.0
oslo.context==2.19.2
oslo.i18n==3.15.3
oslo.log==3.36.0

View File

@ -6,7 +6,7 @@ pbr!=2.1.0,>=2.0.0 # Apache-2.0
Babel!=2.4.0,>=2.3.4 # BSD
cryptography>=2.1 # BSD/Apache-2.0
python-barbicanclient>=4.5.2 # Apache-2.0
oslo.config>=5.2.0 # Apache-2.0
oslo.config>=6.4.0 # Apache-2.0
oslo.context>=2.19.2 # Apache-2.0
oslo.i18n>=3.15.3 # Apache-2.0
oslo.log>=3.36.0 # Apache-2.0

View File

@ -27,6 +27,9 @@ oslo.config.opts =
castellan.tests.functional.config = castellan.tests.functional.config:list_opts
castellan.config = castellan.options:list_opts
oslo.config.driver =
castellan = castellan._config_driver:CastellanConfigurationSourceDriver
castellan.drivers =
barbican = castellan.key_manager.barbican_key_manager:BarbicanKeyManager
vault = castellan.key_manager.vault_key_manager:VaultKeyManager

View File

@ -10,6 +10,7 @@ sphinx!=1.6.6,!=1.6.7,>=1.6.2 # BSD
openstackdocstheme>=1.18.1 # Apache-2.0
oslotest>=3.2.0 # Apache-2.0
stestr>=2.0.0 # Apache-2.0
fixtures>=3.0.0 # Apache-2.0/BSD
testscenarios>=0.4 # Apache-2.0/BSD
testtools>=2.2.0 # MIT
bandit>=1.1.0 # Apache-2.0