summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--castellan/_config_driver.py141
-rw-r--r--castellan/tests/unit/test_config_driver.py108
-rw-r--r--lower-constraints.txt2
-rw-r--r--requirements.txt2
-rw-r--r--setup.cfg3
-rw-r--r--test-requirements.txt1
7 files changed, 256 insertions, 2 deletions
diff --git a/.gitignore b/.gitignore
index 3edc8ef..304efbf 100644
--- a/.gitignore
+++ b/.gitignore
@@ -28,6 +28,7 @@ pip-log.txt
28.stestr/ 28.stestr/
29.venv 29.venv
30cover 30cover
31vault_*
31 32
32# Translations 33# Translations
33*.mo 34*.mo
diff --git a/castellan/_config_driver.py b/castellan/_config_driver.py
new file mode 100644
index 0000000..26fdb0a
--- /dev/null
+++ b/castellan/_config_driver.py
@@ -0,0 +1,141 @@
1# Licensed under the Apache License, Version 2.0 (the "License"); you may
2# not use this file except in compliance with the License. You may obtain
3# a copy of the License at
4#
5# http://www.apache.org/licenses/LICENSE-2.0
6#
7# Unless required by applicable law or agreed to in writing, software
8# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10# License for the specific language governing permissions and limitations
11# under the License.
12
13r"""
14Castellan Oslo Config Driver
15----------------------------
16
17This driver is an oslo.config backend driver implemented with Castellan. It
18extends oslo.config's capabilities by enabling it to retrieve configuration
19values from a secret manager behind Castellan.
20
21The setup of a Castellan configuration source is as follow::
22
23 [DEFAULT]
24 config_source = castellan_config_group
25
26 [castellan_config_group]
27 driver = castellan
28 config_file = castellan.conf
29 mapping_file = mapping.conf
30
31In the following sessions, you can find more information about this driver's
32classes and its options.
33
34The Driver Class
35================
36
37.. autoclass:: CastellanConfigurationSourceDriver
38
39The Configuration Source Class
40==============================
41
42.. autoclass:: CastellanConfigurationSource
43
44"""
45from castellan.common.exception import KeyManagerError
46from castellan.common.exception import ManagedObjectNotFoundError
47from castellan import key_manager
48
49from oslo_config import cfg
50from oslo_config import sources
51from oslo_log import log
52
53LOG = log.getLogger(__name__)
54
55
56class CastellanConfigurationSourceDriver(sources.ConfigurationSourceDriver):
57 """A backend driver for configuration values served through castellan.
58
59 Required options:
60 - config_file: The castellan configuration file.
61
62 - mapping_file: A configuration/castellan_id mapping file. This file
63 creates connections between configuration options and
64 castellan ids. The group and option name remains the
65 same, while the value gets stored a secret manager behind
66 castellan and is replaced by its castellan id. The ids
67 will be used to fetch the values through castellan.
68 """
69
70 _castellan_driver_opts = [
71 cfg.StrOpt(
72 'config_file',
73 required=True,
74 sample_default='etc/castellan/castellan.conf',
75 help=('The path to a castellan configuration file.'),
76 ),
77 cfg.StrOpt(
78 'mapping_file',
79 required=True,
80 sample_default='etc/castellan/secrets_mapping.conf',
81 help=('The path to a configuration/castellan_id mapping file.'),
82 ),
83 ]
84
85 def list_options_for_discovery(self):
86 return self._castellan_driver_opts
87
88 def open_source_from_opt_group(self, conf, group_name):
89 conf.register_opts(self._castellan_driver_opts, group_name)
90
91 return CastellanConfigurationSource(
92 group_name,
93 conf[group_name].config_file,
94 conf[group_name].mapping_file)
95
96
97class CastellanConfigurationSource(sources.ConfigurationSource):
98 """A configuration source for configuration values served through castellan.
99
100 :param config_file: The path to a castellan configuration file.
101
102 :param mapping_file: The path to a configuration/castellan_id mapping file.
103 """
104
105 def __init__(self, group_name, config_file, mapping_file):
106 conf = cfg.ConfigOpts()
107 conf(args=[], default_config_files=[config_file])
108
109 self._name = group_name
110 self._mngr = key_manager.API(conf)
111 self._mapping = {}
112
113 cfg.ConfigParser(mapping_file, self._mapping).parse()
114
115 def get(self, group_name, option_name, opt):
116 try:
117 group_name = group_name or "DEFAULT"
118
119 castellan_id = self._mapping[group_name][option_name][0]
120
121 return (self._mngr.get("ctx", castellan_id).get_encoded().decode(),
122 cfg.LocationInfo(cfg.Locations.user, castellan_id))
123
124 except KeyError:
125 # no mapping 'option = castellan_id'
126 LOG.info("option '[%s] %s' not present in '[%s] mapping_file'",
127 group_name, option_name, self._name)
128
129 except KeyManagerError:
130 # bad mapping 'option =' without a castellan_id
131 LOG.warning("missing castellan_id for option "
132 "'[%s] %s' in '[%s] mapping_file'",
133 group_name, option_name, self._name)
134
135 except ManagedObjectNotFoundError:
136 # good mapping, but unknown castellan_id by secret manager
137 LOG.warning("invalid castellan_id for option "
138 "'[%s] %s' in '[%s] mapping_file'",
139 group_name, option_name, self._name)
140
141 return (sources._NoValue, None)
diff --git a/castellan/tests/unit/test_config_driver.py b/castellan/tests/unit/test_config_driver.py
new file mode 100644
index 0000000..5b3275d
--- /dev/null
+++ b/castellan/tests/unit/test_config_driver.py
@@ -0,0 +1,108 @@
1# Licensed under the Apache License, Version 2.0 (the "License"); you may
2# not use this file except in compliance with the License. You may obtain
3# a copy of the License at
4#
5# http://www.apache.org/licenses/LICENSE-2.0
6#
7# Unless required by applicable law or agreed to in writing, software
8# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10# License for the specific language governing permissions and limitations
11# under the License.
12
13"""
14Functional test cases for the Castellan Oslo Config Driver.
15
16Note: This requires local running instance of Vault.
17"""
18import tempfile
19
20from oslo_config import cfg
21from oslo_config import fixture
22
23from oslotest import base
24
25from castellan import _config_driver
26from castellan.common.objects import opaque_data
27from castellan.tests.unit.key_manager import fake
28
29
30class CastellanSourceTestCase(base.BaseTestCase):
31
32 def setUp(self):
33 super(CastellanSourceTestCase, self).setUp()
34 self.driver = _config_driver.CastellanConfigurationSourceDriver()
35 self.conf = cfg.ConfigOpts()
36 self.conf_fixture = self.useFixture(fixture.Config(self.conf))
37
38 def test_incomplete_driver(self):
39 # The group exists, but does not specify the
40 # required options for this driver.
41 self.conf_fixture.load_raw_values(
42 group='incomplete_driver',
43 driver='castellan',
44 )
45 source = self.conf._open_source_from_opt_group('incomplete_driver')
46
47 self.assertIsNone(source)
48 self.assertEqual(self.conf.incomplete_driver.driver, 'castellan')
49
50 def test_complete_driver(self):
51 self.conf_fixture.load_raw_values(
52 group='castellan_source',
53 driver='castellan',
54 config_file='config.conf',
55 mapping_file='mapping.conf',
56 )
57
58 with base.mock.patch.object(
59 _config_driver,
60 'CastellanConfigurationSource') as source_class:
61 self.driver.open_source_from_opt_group(
62 self.conf, 'castellan_source')
63
64 source_class.assert_called_once_with(
65 'castellan_source',
66 self.conf.castellan_source.config_file,
67 self.conf.castellan_source.mapping_file)
68
69 def test_fetch_secret(self):
70 # fake KeyManager populated with secret
71 km = fake.fake_api()
72 secret_id = km.store("fake_context",
73 opaque_data.OpaqueData(b"super_secret!"))
74
75 # driver config
76 config = "[key_manager]\nbackend=vault"
77 mapping = "[DEFAULT]\nmy_secret=" + secret_id
78
79 # creating temp files
80 with tempfile.NamedTemporaryFile() as config_file:
81 config_file.write(config.encode("utf-8"))
82 config_file.flush()
83
84 with tempfile.NamedTemporaryFile() as mapping_file:
85 mapping_file.write(mapping.encode("utf-8"))
86 mapping_file.flush()
87
88 self.conf_fixture.load_raw_values(
89 group='castellan_source',
90 driver='castellan',
91 config_file=config_file.name,
92 mapping_file=mapping_file.name,
93 )
94
95 source = self.driver.open_source_from_opt_group(
96 self.conf,
97 'castellan_source')
98
99 # replacing key_manager with fake one
100 source._mngr = km
101
102 # testing if the source is able to retrieve
103 # the secret value stored in the key_manager
104 # using the secret_id from the mapping file
105 self.assertEqual("super_secret!",
106 source.get("DEFAULT",
107 "my_secret",
108 cfg.StrOpt(""))[0])
diff --git a/lower-constraints.txt b/lower-constraints.txt
index b51623b..3432ac9 100644
--- a/lower-constraints.txt
+++ b/lower-constraints.txt
@@ -33,7 +33,7 @@ netaddr==0.7.18
33netifaces==0.10.4 33netifaces==0.10.4
34openstackdocstheme==1.18.1 34openstackdocstheme==1.18.1
35os-client-config==1.28.0 35os-client-config==1.28.0
36oslo.config==5.2.0 36oslo.config==6.4.0
37oslo.context==2.19.2 37oslo.context==2.19.2
38oslo.i18n==3.15.3 38oslo.i18n==3.15.3
39oslo.log==3.36.0 39oslo.log==3.36.0
diff --git a/requirements.txt b/requirements.txt
index a6020f7..eeeb417 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -6,7 +6,7 @@ pbr!=2.1.0,>=2.0.0 # Apache-2.0
6Babel!=2.4.0,>=2.3.4 # BSD 6Babel!=2.4.0,>=2.3.4 # BSD
7cryptography>=2.1 # BSD/Apache-2.0 7cryptography>=2.1 # BSD/Apache-2.0
8python-barbicanclient>=4.5.2 # Apache-2.0 8python-barbicanclient>=4.5.2 # Apache-2.0
9oslo.config>=5.2.0 # Apache-2.0 9oslo.config>=6.4.0 # Apache-2.0
10oslo.context>=2.19.2 # Apache-2.0 10oslo.context>=2.19.2 # Apache-2.0
11oslo.i18n>=3.15.3 # Apache-2.0 11oslo.i18n>=3.15.3 # Apache-2.0
12oslo.log>=3.36.0 # Apache-2.0 12oslo.log>=3.36.0 # Apache-2.0
diff --git a/setup.cfg b/setup.cfg
index 993a292..63bacbe 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -27,6 +27,9 @@ oslo.config.opts =
27 castellan.tests.functional.config = castellan.tests.functional.config:list_opts 27 castellan.tests.functional.config = castellan.tests.functional.config:list_opts
28 castellan.config = castellan.options:list_opts 28 castellan.config = castellan.options:list_opts
29 29
30oslo.config.driver =
31 castellan = castellan._config_driver:CastellanConfigurationSourceDriver
32
30castellan.drivers = 33castellan.drivers =
31 barbican = castellan.key_manager.barbican_key_manager:BarbicanKeyManager 34 barbican = castellan.key_manager.barbican_key_manager:BarbicanKeyManager
32 vault = castellan.key_manager.vault_key_manager:VaultKeyManager 35 vault = castellan.key_manager.vault_key_manager:VaultKeyManager
diff --git a/test-requirements.txt b/test-requirements.txt
index b35c1cb..918095d 100644
--- a/test-requirements.txt
+++ b/test-requirements.txt
@@ -10,6 +10,7 @@ sphinx!=1.6.6,!=1.6.7,>=1.6.2 # BSD
10openstackdocstheme>=1.18.1 # Apache-2.0 10openstackdocstheme>=1.18.1 # Apache-2.0
11oslotest>=3.2.0 # Apache-2.0 11oslotest>=3.2.0 # Apache-2.0
12stestr>=2.0.0 # Apache-2.0 12stestr>=2.0.0 # Apache-2.0
13fixtures>=3.0.0 # Apache-2.0/BSD
13testscenarios>=0.4 # Apache-2.0/BSD 14testscenarios>=0.4 # Apache-2.0/BSD
14testtools>=2.2.0 # MIT 15testtools>=2.2.0 # MIT
15bandit>=1.1.0 # Apache-2.0 16bandit>=1.1.0 # Apache-2.0