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