Codebase list python-castellan / bfae17a
Merge tag '1.2.2' into debian/stein castellan 1.2.2 release meta:version: 1.2.2 meta:diff-start: - meta:series: stein meta:release-type: release meta:pypi: no meta:first: no meta:release:Author: Ben Nemec <bnemec@redhat.com> meta:release:Commit: Ben Nemec <bnemec@redhat.com> meta:release:Change-Id: I4f3b9ea53f61b869bcc329ea18990795f40a4769 meta:release:Code-Review+2: Sean McGinnis <sean.mcginnis@gmail.com> meta:release:Workflow+1: Sean McGinnis <sean.mcginnis@gmail.com> Thomas Goirand 4 years ago
21 changed file(s) with 618 addition(s) and 112 deletion(s). Raw diff Collapse all Expand all
2424 # Unit test / coverage reports
2525 .coverage
2626 .tox
27 nosetests.xml
28 .testrepository
27 .stestr/
2928 .venv
3029 cover
30 vault_*
3131
3232 # Translations
3333 *.mo
0 [DEFAULT]
1 test_path=./castellan/tests
2 top_dir=./
3
+0
-7
.testr.conf less more
0 [DEFAULT]
1 test_command=OS_STDOUT_CAPTURE=${OS_STDOUT_CAPTURE:-1} \
2 OS_STDERR_CAPTURE=${OS_STDERR_CAPTURE:-1} \
3 OS_TEST_TIMEOUT=${OS_TEST_TIMEOUT:-60} \
4 ${PYTHON:-python} -m subunit.run discover -t ./ ${OS_TEST_PATH:-./castellan/tests} $LISTOPT $IDOPTION
5 test_id_option=--load-list $IDFILE
6 test_list_option=--list
5050 jobs:
5151 - castellan-functional-vault
5252 - castellan-functional-devstack
53 - openstack-tox-lower-constraints
5453 - barbican-simple-crypto-devstack-tempest-castellan-from-git
5554 gate:
5655 jobs:
5756 - castellan-functional-vault
5857 - castellan-functional-devstack
59 - openstack-tox-lower-constraints
6058 - barbican-simple-crypto-devstack-tempest-castellan-from-git
6159 templates:
60 - check-requirements
61 - openstack-lower-constraints-jobs
6262 - openstack-python-jobs
63 - openstack-python35-jobs
63 - openstack-python36-jobs
64 - openstack-python37-jobs
65 - periodic-stable-jobs
66 - publish-openstack-docs-pti
6467 - release-notes-jobs-python3
65 - publish-openstack-docs-pti
66 - check-requirements
67 - periodic-stable-jobs
77 * Documentation: https://docs.openstack.org/castellan/latest
88 * Source: https://git.openstack.org/cgit/openstack/castellan
99 * Bugs: https://bugs.launchpad.net/castellan
10 * Release notes: https://docs.openstack.org/releasenotes/castellan
1011
1112 Team and repository tags
1213 ========================
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)
2828 from keystoneauth1 import loading
2929 from oslo_config import cfg
3030 from oslo_log import log as logging
31 from oslo_utils import timeutils
3132 import requests
3233 import six
3334
4243 from castellan.key_manager import key_manager
4344
4445 DEFAULT_VAULT_URL = "http://127.0.0.1:8200"
46 DEFAULT_MOUNTPOINT = "secret"
4547
4648 vault_opts = [
4749 cfg.StrOpt('root_token_id',
4850 help='root token for vault'),
51 cfg.StrOpt('approle_role_id',
52 help='AppRole role_id for authentication with vault'),
53 cfg.StrOpt('approle_secret_id',
54 help='AppRole secret_id for authentication with vault'),
55 cfg.StrOpt('kv_mountpoint',
56 default=DEFAULT_MOUNTPOINT,
57 help='Mountpoint of KV store in Vault to use, for example: '
58 '{}'.format(DEFAULT_MOUNTPOINT)),
4959 cfg.StrOpt('vault_url',
5060 default=DEFAULT_VAULT_URL,
5161 help='Use this endpoint to connect to Vault, for example: '
8797 loading.register_session_conf_options(self._conf, VAULT_OPT_GROUP)
8898 self._session = requests.Session()
8999 self._root_token_id = self._conf.vault.root_token_id
100 self._approle_role_id = self._conf.vault.approle_role_id
101 self._approle_secret_id = self._conf.vault.approle_secret_id
102 self._cached_approle_token_id = None
103 self._approle_token_ttl = None
104 self._approle_token_issue = None
105 self._kv_mountpoint = self._conf.vault.kv_mountpoint
90106 self._vault_url = self._conf.vault.vault_url
91107 if self._vault_url.startswith("https://"):
92108 self._verify_server = self._conf.vault.ssl_ca_crt_file or True
93109 else:
94110 self._verify_server = False
111 self._vault_kv_version = None
95112
96113 def _get_url(self):
97114 if not self._vault_url.endswith('/'):
98115 self._vault_url += '/'
99116 return self._vault_url
117
118 def _get_api_version(self):
119 if self._vault_kv_version:
120 return self._vault_kv_version
121
122 resource_url = '{}v1/sys/internal/ui/mounts/{}'.format(
123 self._get_url(),
124 self._kv_mountpoint
125 )
126 resp = self._do_http_request(self._session.get, resource_url)
127
128 if resp.status_code == requests.codes['not_found']:
129 self._vault_kv_version = '1'
130 else:
131 self._vault_kv_version = resp.json()['data']['options']['version']
132
133 return self._vault_kv_version
134
135 def _get_resource_url(self, key_id=None):
136 return '{}v1/{}/{}{}'.format(
137 self._get_url(),
138 self._kv_mountpoint,
139
140 '' if self._get_api_version() == '1' else
141 'data/' if key_id else
142 'metadata/', # no key_id is for listing and 'data/' doesn't works
143
144 key_id if key_id else '?list=true')
145
146 @property
147 def _approle_token_id(self):
148 if (all((self._approle_token_issue, self._approle_token_ttl)) and
149 timeutils.is_older_than(self._approle_token_issue,
150 self._approle_token_ttl)):
151 self._cached_approle_token_id = None
152 return self._cached_approle_token_id
153
154 def _build_auth_headers(self):
155 if self._root_token_id:
156 return {'X-Vault-Token': self._root_token_id}
157
158 if self._approle_token_id:
159 return {'X-Vault-Token': self._approle_token_id}
160
161 if self._approle_role_id:
162 params = {
163 'role_id': self._approle_role_id
164 }
165 if self._approle_secret_id:
166 params['secret_id'] = self._approle_secret_id
167 approle_login_url = '{}v1/auth/approle/login'.format(
168 self._get_url()
169 )
170 token_issue_utc = timeutils.utcnow()
171 try:
172 resp = self._session.post(url=approle_login_url,
173 json=params,
174 verify=self._verify_server)
175 except requests.exceptions.Timeout as ex:
176 raise exception.KeyManagerError(six.text_type(ex))
177 except requests.exceptions.ConnectionError as ex:
178 raise exception.KeyManagerError(six.text_type(ex))
179 except Exception as ex:
180 raise exception.KeyManagerError(six.text_type(ex))
181
182 if resp.status_code in _EXCEPTIONS_BY_CODE:
183 raise exception.KeyManagerError(resp.reason)
184 if resp.status_code == requests.codes['forbidden']:
185 raise exception.Forbidden()
186
187 resp = resp.json()
188 self._cached_approle_token_id = resp['auth']['client_token']
189 self._approle_token_issue = token_issue_utc
190 self._approle_token_ttl = resp['auth']['lease_duration']
191 return {'X-Vault-Token': self._approle_token_id}
192
193 return {}
194
195 def _do_http_request(self, method, resource, json=None):
196 verify = self._verify_server
197 headers = self._build_auth_headers()
198
199 try:
200 resp = method(resource, headers=headers, json=json, verify=verify)
201 except requests.exceptions.Timeout as ex:
202 raise exception.KeyManagerError(six.text_type(ex))
203 except requests.exceptions.ConnectionError as ex:
204 raise exception.KeyManagerError(six.text_type(ex))
205 except Exception as ex:
206 raise exception.KeyManagerError(six.text_type(ex))
207
208 if resp.status_code in _EXCEPTIONS_BY_CODE:
209 raise exception.KeyManagerError(resp.reason)
210 if resp.status_code == requests.codes['forbidden']:
211 raise exception.Forbidden()
212
213 return resp
100214
101215 def create_key_pair(self, context, algorithm, length,
102216 expiration=None, name=None):
156270 raise exception.KeyManagerError(
157271 "Unknown type for value : %r" % value)
158272
159 headers = {'X-Vault-Token': self._root_token_id}
160 try:
161 resource_url = self._get_url() + 'v1/secret/' + key_id
162 record = {
163 'type': type_value,
164 'value': binascii.hexlify(value.get_encoded()).decode('utf-8'),
165 'algorithm': (value.algorithm if hasattr(value, 'algorithm')
166 else None),
167 'bit_length': (value.bit_length if hasattr(value, 'bit_length')
168 else None),
169 'name': value.name,
170 'created': value.created
171 }
172 resp = self._session.post(resource_url,
173 verify=self._verify_server,
174 json=record,
175 headers=headers)
176 except requests.exceptions.Timeout as ex:
177 raise exception.KeyManagerError(six.text_type(ex))
178 except requests.exceptions.ConnectionError as ex:
179 raise exception.KeyManagerError(six.text_type(ex))
180 except Exception as ex:
181 raise exception.KeyManagerError(six.text_type(ex))
182
183 if resp.status_code in _EXCEPTIONS_BY_CODE:
184 raise exception.KeyManagerError(resp.reason)
185 if resp.status_code == requests.codes['forbidden']:
186 raise exception.Forbidden()
273 record = {
274 'type': type_value,
275 'value': binascii.hexlify(value.get_encoded()).decode('utf-8'),
276 'algorithm': (value.algorithm if hasattr(value, 'algorithm')
277 else None),
278 'bit_length': (value.bit_length if hasattr(value, 'bit_length')
279 else None),
280 'name': value.name,
281 'created': value.created
282 }
283 if self._get_api_version() != '1':
284 record = {'data': record}
285
286 self._do_http_request(self._session.post,
287 self._get_resource_url(key_id),
288 json=record)
187289
188290 return key_id
189291
195297 msg = _("User is not authorized to use key manager.")
196298 raise exception.Forbidden(msg)
197299
300 if length % 8:
301 msg = _("Length must be multiple of 8.")
302 raise ValueError(msg)
303
198304 key_id = uuid.uuid4().hex
199 key_value = os.urandom(length or 32)
305 key_value = os.urandom((length or 256) // 8)
200306 key = sym_key.SymmetricKey(algorithm,
201 length or 32,
307 length or 256,
202308 key_value,
203309 key_id,
204310 name or int(time.time()))
311
205312 return self._store_key_value(key_id, key)
206313
207314 def store(self, context, key_value, **kwargs):
226333 if not key_id:
227334 raise exception.KeyManagerError('key identifier not provided')
228335
229 headers = {'X-Vault-Token': self._root_token_id}
230 try:
231 resource_url = self._get_url() + 'v1/secret/' + key_id
232 resp = self._session.get(resource_url,
233 verify=self._verify_server,
234 headers=headers)
235 except requests.exceptions.Timeout as ex:
236 raise exception.KeyManagerError(six.text_type(ex))
237 except requests.exceptions.ConnectionError as ex:
238 raise exception.KeyManagerError(six.text_type(ex))
239 except Exception as ex:
240 raise exception.KeyManagerError(six.text_type(ex))
241
242 if resp.status_code in _EXCEPTIONS_BY_CODE:
243 raise exception.KeyManagerError(resp.reason)
244 if resp.status_code == requests.codes['forbidden']:
245 raise exception.Forbidden()
336 resp = self._do_http_request(self._session.get,
337 self._get_resource_url(key_id))
338
246339 if resp.status_code == requests.codes['not_found']:
247340 raise exception.ManagedObjectNotFoundError(uuid=key_id)
248341
249342 record = resp.json()['data']
343 if self._get_api_version() != '1':
344 record = record['data']
345
250346 key = None if metadata_only else binascii.unhexlify(record['value'])
251347
252348 clazz = None
282378 if not key_id:
283379 raise exception.KeyManagerError('key identifier not provided')
284380
285 headers = {'X-Vault-Token': self._root_token_id}
286 try:
287 resource_url = self._get_url() + 'v1/secret/' + key_id
288 resp = self._session.delete(resource_url,
289 verify=self._verify_server,
290 headers=headers)
291 except requests.exceptions.Timeout as ex:
292 raise exception.KeyManagerError(six.text_type(ex))
293 except requests.exceptions.ConnectionError as ex:
294 raise exception.KeyManagerError(six.text_type(ex))
295 except Exception as ex:
296 raise exception.KeyManagerError(six.text_type(ex))
297
298 if resp.status_code in _EXCEPTIONS_BY_CODE:
299 raise exception.KeyManagerError(resp.reason)
300 if resp.status_code == requests.codes['forbidden']:
301 raise exception.Forbidden()
381 resp = self._do_http_request(self._session.delete,
382 self._get_resource_url(key_id))
383
302384 if resp.status_code == requests.codes['not_found']:
303385 raise exception.ManagedObjectNotFoundError(uuid=key_id)
304386
314396 msg = _("Invalid secret type: %s") % object_type
315397 raise exception.KeyManagerError(reason=msg)
316398
317 headers = {'X-Vault-Token': self._root_token_id}
318 try:
319 resource_url = self._get_url() + 'v1/secret/?list=true'
320 resp = self._session.get(resource_url,
321 verify=self._verify_server,
322 headers=headers)
323 keys = resp.json()['data']['keys']
324 except requests.exceptions.Timeout as ex:
325 raise exception.KeyManagerError(six.text_type(ex))
326 except requests.exceptions.ConnectionError as ex:
327 raise exception.KeyManagerError(six.text_type(ex))
328 except Exception as ex:
329 raise exception.KeyManagerError(six.text_type(ex))
330
331 if resp.status_code in _EXCEPTIONS_BY_CODE:
332 raise exception.KeyManagerError(resp.reason)
333 if resp.status_code == requests.codes['forbidden']:
334 raise exception.Forbidden()
399 resp = self._do_http_request(self._session.get,
400 self._get_resource_url())
401
335402 if resp.status_code == requests.codes['not_found']:
336403 keys = []
404 else:
405 keys = resp.json()['data']['keys']
337406
338407 objects = []
339408 for obj_id in keys:
3838 def set_defaults(conf, backend=None, barbican_endpoint=None,
3939 barbican_api_version=None, auth_endpoint=None,
4040 retry_delay=None, number_of_retries=None, verify_ssl=None,
41 api_class=None, vault_root_token_id=None, vault_url=None,
41 api_class=None, vault_root_token_id=None,
42 vault_approle_role_id=None, vault_approle_secret_id=None,
43 vault_kv_mountpoint=None, vault_url=None,
4244 vault_ssl_ca_crt_file=None, vault_use_ssl=None,
4345 barbican_endpoint_type=None):
4446 """Set defaults for configuration values.
5355 :param number_of_retries: Use this attribute to set number of retries.
5456 :param verify_ssl: Use this to specify if ssl should be verified.
5557 :param vault_root_token_id: Use this for the root token id for vault.
58 :param vault_approle_role_id: Use this for the approle role_id for vault.
59 :param vault_approle_secret_id: Use this for the approle secret_id
60 for vault.
61 :param vault_kv_mountpoint: Mountpoint of KV store in vault to use.
5662 :param vault_url: Use this for the url for vault.
5763 :param vault_use_ssl: Use this to force vault driver to use ssl.
5864 :param vault_ssl_ca_crt_file: Use this for the CA file for vault.
96102 if vkm is not None:
97103 if vault_root_token_id is not None:
98104 conf.set_default('root_token_id', vault_root_token_id,
105 group=vkm.VAULT_OPT_GROUP)
106 if vault_approle_role_id is not None:
107 conf.set_default('approle_role_id', vault_approle_role_id,
108 group=vkm.VAULT_OPT_GROUP)
109 if vault_approle_secret_id is not None:
110 conf.set_default('approle_secret_id', vault_approle_secret_id,
111 group=vkm.VAULT_OPT_GROUP)
112 if vault_kv_mountpoint is not None:
113 conf.set_default('kv_mountpoint', vault_kv_mountpoint,
99114 group=vkm.VAULT_OPT_GROUP)
100115 if vault_url is not None:
101116 conf.set_default('vault_url', vault_url,
1616 """
1717 import abc
1818 import os
19 import uuid
1920
2021 from oslo_config import cfg
2122 from oslo_context import context
2223 from oslo_utils import uuidutils
2324 from oslotest import base
25 import requests
2426 from testtools import testcase
2527
2628 from castellan.common import exception
108110 base.BaseTestCase):
109111 def get_context(self):
110112 return context.get_admin_context()
113
114
115 TEST_POLICY = '''
116 path "{backend}/*" {{
117 capabilities = ["create", "read", "update", "delete", "list"]
118 }}
119
120 path "sys/internal/ui/mounts/{backend}" {{
121 capabilities = ["read"]
122 }}
123 '''
124
125 AUTH_ENDPOINT = 'v1/sys/auth/{auth_type}'
126 POLICY_ENDPOINT = 'v1/sys/policy/{policy_name}'
127 APPROLE_ENDPOINT = 'v1/auth/approle/role/{role_name}'
128
129
130 class VaultKeyManagerAppRoleTestCase(VaultKeyManagerOSLOContextTestCase):
131
132 mountpoint = 'secret'
133
134 def _create_key_manager(self):
135 key_mgr = vault_key_manager.VaultKeyManager(cfg.CONF)
136
137 if ('VAULT_TEST_URL' not in os.environ or
138 'VAULT_TEST_ROOT_TOKEN' not in os.environ):
139 raise testcase.TestSkipped('Missing Vault setup information')
140
141 self.root_token_id = os.environ['VAULT_TEST_ROOT_TOKEN']
142 self.vault_url = os.environ['VAULT_TEST_URL']
143
144 test_uuid = str(uuid.uuid4())
145 vault_policy = 'policy-{}'.format(test_uuid)
146 vault_approle = 'approle-{}'.format(test_uuid)
147
148 self.session = requests.Session()
149 self.session.headers.update({'X-Vault-Token': self.root_token_id})
150
151 self._mount_kv(self.mountpoint)
152 self._enable_approle()
153 self._create_policy(vault_policy)
154 self._create_approle(vault_approle, vault_policy)
155
156 key_mgr._approle_role_id, key_mgr._approle_secret_id = (
157 self._retrieve_approle(vault_approle)
158 )
159 key_mgr._kv_mountpoint = self.mountpoint
160 key_mgr._vault_url = self.vault_url
161 return key_mgr
162
163 def _mount_kv(self, vault_mountpoint):
164 backends = self.session.get(
165 '{}/v1/sys/mounts'.format(self.vault_url)).json()
166 if vault_mountpoint not in backends:
167 params = {
168 'type': 'kv',
169 'options': {
170 'version': 2,
171 }
172 }
173 self.session.post(
174 '{}/v1/sys/mounts/{}'.format(self.vault_url,
175 vault_mountpoint),
176 json=params)
177
178 def _enable_approle(self):
179 params = {
180 'type': 'approle'
181 }
182 self.session.post(
183 '{}/{}'.format(
184 self.vault_url,
185 AUTH_ENDPOINT.format(auth_type='approle')
186 ),
187 json=params,
188 )
189
190 def _create_policy(self, vault_policy):
191 params = {
192 'rules': TEST_POLICY.format(backend=self.mountpoint),
193 }
194 self.session.put(
195 '{}/{}'.format(
196 self.vault_url,
197 POLICY_ENDPOINT.format(policy_name=vault_policy)
198 ),
199 json=params,
200 )
201
202 def _create_approle(self, vault_approle, vault_policy):
203 params = {
204 'token_ttl': '60s',
205 'token_max_ttl': '60s',
206 'policies': [vault_policy],
207 'bind_secret_id': 'true',
208 'bound_cidr_list': '127.0.0.1/32'
209 }
210 self.session.post(
211 '{}/{}'.format(
212 self.vault_url,
213 APPROLE_ENDPOINT.format(role_name=vault_approle)
214 ),
215 json=params,
216 )
217
218 def _retrieve_approle(self, vault_approle):
219 approle_role_id = (
220 self.session.get(
221 '{}/v1/auth/approle/role/{}/role-id'.format(
222 self.vault_url,
223 vault_approle
224 )).json()['data']['role_id']
225 )
226 approle_secret_id = (
227 self.session.post(
228 '{}/v1/auth/approle/role/{}/secret-id'.format(
229 self.vault_url,
230 vault_approle
231 )).json()['data']['secret_id']
232 )
233 return (approle_role_id, approle_secret_id)
234
235
236 class VaultKeyManagerAltMountpointTestCase(VaultKeyManagerAppRoleTestCase):
237
238 mountpoint = 'different-secrets'
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])
3232 netifaces==0.10.4
3333 openstackdocstheme==1.18.1
3434 os-client-config==1.28.0
35 oslo.config==5.2.0
35 oslo.config==6.4.0
3636 oslo.context==2.19.2
3737 oslo.i18n==3.15.3
3838 oslo.log==3.36.0
6565 Sphinx==1.6.2
6666 sphinxcontrib-websupport==1.0.1
6767 stevedore==1.20.0
68 testrepository==0.0.18
68 stestr==2.0.0
6969 testscenarios==0.4
7070 testtools==2.2.0
7171 traceback2==1.4.0
0 ---
1 fixes:
2 - |
3 Fixed VaultKeyManager.create_key() to consider the `length` param as bits
4 instead of bytes for the key length. This was causing a discrepancy between
5 keys generated by the HashiCorp Vault backend and the OpenStack Barbican
6 backend. Considering `km` as an instance of a key manager, the following
7 code `km.create_key(ctx, "AES", 256)` was generating a 256 bit AES key when
8 Barbican is configured as the backend, but generating a 2048 bit AES key
9 when Vault was configured as the backend.
0 ---
1 features:
2 - |
3 Added support for AppRole based authentication to the Vault
4 key manager configured using new approle_role_id and
5 optional approle_secret_id options.
6 (https://www.vaultproject.io/docs/auth/approle.html)
0 ---
1 features:
2 - |
3 Added configuration option to the Vault key manager to allow
4 the KV store mountpoint in Vault to be specified; the existing
5 default of 'secret' is maintained.
55 :maxdepth: 1
66
77 unreleased
8 rocky
89 queens
910 pike
0 ===================================
1 Rocky Series Release Notes
2 ===================================
3
4 .. release-notes::
5 :branch: stable/rocky
55 Babel!=2.4.0,>=2.3.4 # BSD
66 cryptography>=2.1 # BSD/Apache-2.0
77 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
99 oslo.context>=2.19.2 # Apache-2.0
1010 oslo.i18n>=3.15.3 # Apache-2.0
1111 oslo.log>=3.36.0 # Apache-2.0
33 description-file =
44 README.rst
55 author = OpenStack
6 author-email = openstack-dev@lists.openstack.org
6 author-email = openstack-discuss@lists.openstack.org
77 home-page = https://docs.openstack.org/castellan/latest/
88 classifier =
99 Environment :: OpenStack
2525 oslo.config.opts =
2626 castellan.tests.functional.config = castellan.tests.functional.config:list_opts
2727 castellan.config = castellan.options:list_opts
28
29 oslo.config.driver =
30 castellan = castellan._config_driver:CastellanConfigurationSourceDriver
2831
2932 castellan.drivers =
3033 barbican = castellan.key_manager.barbican_key_manager:BarbicanKeyManager
88 sphinx!=1.6.6,!=1.6.7,>=1.6.2 # BSD
99 openstackdocstheme>=1.18.1 # Apache-2.0
1010 oslotest>=3.2.0 # Apache-2.0
11 testrepository>=0.0.18 # Apache-2.0/BSD
11 stestr>=2.0.0 # Apache-2.0
12 fixtures>=3.0.0 # Apache-2.0/BSD
1213 testscenarios>=0.4 # Apache-2.0/BSD
1314 testtools>=2.2.0 # MIT
1415 bandit>=1.1.0 # Apache-2.0
00 #!/bin/bash
11 set -eux
22 if [ -z "$(which vault)" ]; then
3 VAULT_VERSION=0.7.3
3 VAULT_VERSION=0.10.4
44 SUFFIX=zip
55 case `uname -s` in
66 Darwin)
00 [tox]
1 minversion = 1.6
2 envlist = py35,py27,pep8
1 minversion = 2.0
2 envlist = py36,py27,pep8
33 skipsdist = True
44
55 [testenv]
6 basepython = python3
76 usedevelop = True
87 install_command = pip install {opts} {packages}
98 setenv =
1312 -c{env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt?h=stable/rocky}
1413 -r{toxinidir}/requirements.txt
1514 -r{toxinidir}/test-requirements.txt
16 commands = python setup.py testr --slowest --testr-args='{posargs}'
15 commands = stestr run --slowest {posargs}
1716
1817 [testenv:py27]
1918 basepython = python2.7
2019
2120 [testenv:pep8]
21 basepython = python3
2222 commands =
2323 flake8
2424 bandit -r castellan -x tests -s B105,B106,B107,B607
2525
2626 [testenv:bandit]
27 basepython = python3
2728 # This command runs the bandit security linter against the castellan
2829 # codebase minus the tests directory. Some tests are being excluded to
2930 # reduce the number of positives before a team inspection, and to ensure a
3637 bandit -r castellan -x tests -s B105,B106,B107,B607
3738
3839 [testenv:venv]
40 basepython = python3
3941 commands = {posargs}
4042
4143 [testenv:debug]
44 basepython = python3
4245 commands = oslo_debug_helper {posargs}
4346
4447 [testenv:cover]
48 basepython = python3
49 setenv =
50 PYTHON=coverage run --source $project --parallel-mode
4551 commands =
46 python setup.py testr --coverage --testr-args='{posargs}'
47 coverage report
52 stestr run {posargs}
53 coverage combine
54 coverage html -d cover
55 coverage xml -o cover/coverage.xml
56 coverage report
4857
4958 [testenv:docs]
59 basepython = python3
5060 commands = python setup.py build_sphinx
5161
5262 [testenv:releasenotes]
63 basepython = python3
5364 commands = sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html
5465
5566 [testenv:functional]
5869 setenv =
5970 VIRTUAL_ENV={envdir}
6071 OS_TEST_PATH=./castellan/tests/functional
61 commands = python setup.py testr --slowest --testr-args='{posargs}'
72 commands = stestr run --slowest {posargs}
6273
6374 [testenv:functional-vault]
6475 passenv = HOME
6879 VIRTUAL_ENV={envdir}
6980 OS_TEST_PATH=./castellan/tests/functional
7081 commands =
71 {toxinidir}/tools/setup-vault-env.sh pifpaf -e VAULT_TEST run vault -- python setup.py testr --slowest --testr-args='{posargs}'
82 {toxinidir}/tools/setup-vault-env.sh pifpaf -e VAULT_TEST run vault -- stestr run --slowest {posargs}
7283
7384 [testenv:genconfig]
85 basepython = python3
7486 commands =
7587 oslo-config-generator --config-file=etc/castellan/functional-config-generator.conf
7688 oslo-config-generator --config-file=etc/castellan/sample-config-generator.conf
8698 import_exceptions = castellan.i18n
8799
88100 [testenv:lower-constraints]
101 basepython = python3
89102 deps =
90103 -c{toxinidir}/lower-constraints.txt
91104 -r{toxinidir}/test-requirements.txt