diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9d94556..08aef91 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -9,7 +9,7 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: ebc15addedad713c86ef18ae9632c88e187dd0af # v3.1.0 + rev: 9136088a246768144165fcc3ecc3d31bb686920a # v3.3.0 hooks: - id: trailing-whitespace # Replaces or checks mixed line ending @@ -27,9 +27,13 @@ - id: debug-statements - id: check-yaml files: .*\.(yaml|yml)$ - - repo: https://gitlab.com/pycqa/flake8 - rev: 181bb46098dddf7e2d45319ea654b4b4d58c2840 # 3.8.3 + - repo: local hooks: - id: flake8 + name: flake8 additional_dependencies: - hacking>=3.0.1,<3.1.0 + language: python + entry: flake8 + files: '^.*\.py$' + exclude: '^(doc|releasenotes|tools)/.*$' diff --git a/.zuul.yaml b/.zuul.yaml index 93c64e7..05be42a 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -59,7 +59,7 @@ - barbican-tempest-plugin-simple-crypto-castellan-src templates: - check-requirements - - openstack-python3-wallaby-jobs + - openstack-python3-xena-jobs - periodic-stable-jobs - publish-openstack-docs-pti - release-notes-jobs-python3 diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 4218138..6be9468 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -17,14 +17,14 @@ .. _`Openstack Developers Guide`: https://docs.openstack.org/infra/manual/developers.html -Freenode IRC (Chat) -------------------- -You can find Castellaneers in our publicly accessible channel on `freenode`_ +OFTC IRC (Chat) +--------------- +You can find Castellaneers in our publicly accessible channel on `OFTC`_ ``#openstack-barbican``. All conversations are logged and stored for your convenience at `eavesdrop.openstack.org`_. For more information regarding OpenStack IRC channels please visit the `OpenStack IRC Wiki`_. -.. _`freenode`: https://freenode.net +.. _`OFTC`: https://oftc.net .. _`OpenStack IRC Wiki`: https://wiki.openstack.org/wiki/IRC .. _`eavesdrop.openstack.org`: http://eavesdrop.openstack.org/irclogs/ %23openstack-barbican/ diff --git a/bindep.txt b/bindep.txt index 5b3ec8a..85e0246 100644 --- a/bindep.txt +++ b/bindep.txt @@ -2,3 +2,6 @@ # see https://docs.openstack.org/infra/bindep/ for additional information. unzip + +# PDF Docs package dependencies +tex-gyre [doc platform:dpkg] diff --git a/castellan/key_manager/barbican_key_manager.py b/castellan/key_manager/barbican_key_manager.py index 1b545cc..7c58c4a 100644 --- a/castellan/key_manager/barbican_key_manager.py +++ b/castellan/key_manager/barbican_key_manager.py @@ -25,6 +25,7 @@ from cryptography import x509 as cryptography_x509 from keystoneauth1 import identity from keystoneauth1 import loading +from keystoneauth1 import service_token from keystoneauth1 import session from oslo_config import cfg from oslo_log import log as logging @@ -77,10 +78,28 @@ choices=['public', 'internal', 'admin'], help='Specifies the type of endpoint. Allowed values are: ' 'public, private, and admin'), - + cfg.StrOpt('barbican_region_name', + default=None, + help='Specifies the region of the chosen endpoint.'), + cfg.BoolOpt('send_service_user_token', + default=False, + help=""" +When True, if sending a user token to a REST API, also send a service token. + +Nova often reuses the user token provided to the nova-api to talk to other REST +APIs, such as Cinder, Glance and Neutron. It is possible that while the user +token was valid when the request was made to Nova, the token may expire before +it reaches the other service. To avoid any failures, and to make it clear it is +Nova calling the service on the user's behalf, we include a service token along +with the user token. Should the user's token have expired, a valid service +token ensures the REST API request will still be accepted by the keystone +middleware. +"""), ] + _BARBICAN_OPT_GROUP = 'barbican' +_BARBICAN_SERVICE_USER_OPT_GROUP = 'barbican_service_user' LOG = logging.getLogger(__name__) @@ -95,6 +114,11 @@ self.conf.register_opts(_barbican_opts, group=_BARBICAN_OPT_GROUP) loading.register_session_conf_options(self.conf, _BARBICAN_OPT_GROUP) + loading.register_session_conf_options(self.conf, + _BARBICAN_SERVICE_USER_OPT_GROUP) + loading.register_auth_conf_options(self.conf, + _BARBICAN_SERVICE_USER_OPT_GROUP) + def _get_barbican_client(self, context): """Creates a client to connect to the Barbican service. @@ -141,7 +165,7 @@ def _get_keystone_auth(self, context): if context.__class__.__name__ == 'KeystonePassword': - return identity.Password( + auth = identity.Password( auth_url=context.auth_url, username=context.username, password=context.password, @@ -157,7 +181,7 @@ project_domain_name=context.project_domain_name, reauthenticate=context.reauthenticate) elif context.__class__.__name__ == 'KeystoneToken': - return identity.Token( + auth = identity.Token( auth_url=context.auth_url, token=context.token, trust_id=context.trust_id, @@ -172,9 +196,9 @@ # projects begin to use utils.credential_factory elif context.__class__.__name__ == 'RequestContext': if getattr(context, 'get_auth_plugin', None): - return context.get_auth_plugin() + auth = context.get_auth_plugin() else: - return identity.Token( + auth = identity.Token( auth_url=self.conf.barbican.auth_endpoint, token=context.auth_token, project_id=context.project_id, @@ -187,19 +211,31 @@ LOG.error(msg) raise exception.Forbidden(reason=msg) + if self.conf.barbican.send_service_user_token: + service_auth = loading.load_auth_from_conf_options( + self.conf, + group=_BARBICAN_SERVICE_USER_OPT_GROUP) + auth = service_token.ServiceTokenAuthWrapper( + user_auth=auth, + service_auth=service_auth) + + return auth + def _get_barbican_endpoint(self, auth, sess): - barbican = self.conf.barbican - if barbican.barbican_endpoint: - return barbican.barbican_endpoint + if self.conf.barbican.barbican_endpoint: + return self.conf.barbican.barbican_endpoint elif getattr(auth, 'service_catalog', None): endpoint_data = auth.service_catalog.endpoint_data_for( service_type='key-manager', - interface=barbican.barbican_endpoint_type) + interface=self.conf.barbican.barbican_endpoint_type, + region_name=self.conf.barbican.barbican_region_name) return endpoint_data.url else: - service_parameters = {'service_type': 'key-manager', - 'interface': barbican.barbican_endpoint_type} - return auth.get_endpoint(sess, **service_parameters) + return auth.get_endpoint( + sess, + service_type='key-manager', + interface=self.conf.barbican.barbican_endpoint_type, + region_name=self.conf.barbican.barbican_region_name) def _create_base_url(self, auth, sess, endpoint): api_version = None @@ -207,7 +243,9 @@ api_version = self.conf.barbican.barbican_api_version elif getattr(auth, 'service_catalog', None): endpoint_data = auth.service_catalog.endpoint_data_for( - service_type='key-manager') + service_type='key-manager', + interface=self.conf.barbican.barbican_endpoint_type, + region_name=self.conf.barbican.barbican_region_name) api_version = endpoint_data.api_version elif getattr(auth, 'get_discovery', None): discovery = auth.get_discovery(sess, url=endpoint) @@ -646,4 +684,10 @@ return objects def list_options_for_discovery(self): - return [(_BARBICAN_OPT_GROUP, _barbican_opts)] + barbican_service_user_opts = loading.get_session_conf_options() + barbican_service_user_opts += loading.get_auth_common_conf_options() + + return [ + (_BARBICAN_OPT_GROUP, _barbican_opts), + (_BARBICAN_SERVICE_USER_OPT_GROUP, barbican_service_user_opts), + ] diff --git a/castellan/tests/unit/key_manager/test_barbican_key_manager.py b/castellan/tests/unit/key_manager/test_barbican_key_manager.py index cb48d3c..a746c8e 100644 --- a/castellan/tests/unit/key_manager/test_barbican_key_manager.py +++ b/castellan/tests/unit/key_manager/test_barbican_key_manager.py @@ -20,6 +20,9 @@ from unittest import mock from barbicanclient import exceptions as barbican_exceptions +from keystoneauth1 import identity +from keystoneauth1 import service_token +from oslo_context import context from oslo_utils import timeutils from castellan.common import exception @@ -37,8 +40,10 @@ super(BarbicanKeyManagerTestCase, self).setUp() # Create fake auth_token - self.ctxt = mock.Mock() + self.ctxt = mock.Mock(spec=context.RequestContext) self.ctxt.auth_token = "fake_token" + self.ctxt.project_name = "foo" + self.ctxt.project_domain_name = "foo" # Create mock barbican client self._build_mock_barbican() @@ -75,6 +80,102 @@ self.key_mgr._barbican_client = self.mock_barbican self.key_mgr._current_context = self.ctxt + + def test_barbican_endpoint(self): + endpoint_data = mock.Mock() + endpoint_data.url = 'http://localhost:9311' + + auth = mock.Mock(spec=['service_catalog']) + auth.service_catalog.endpoint_data_for.return_value = endpoint_data + + endpoint = self.key_mgr._get_barbican_endpoint(auth, mock.Mock()) + self.assertEqual(endpoint, 'http://localhost:9311') + auth.service_catalog.endpoint_data_for.assert_called_once_with( + service_type='key-manager', interface='public', + region_name=None) + + def test_barbican_endpoint_with_endpoint_type(self): + self.key_mgr.conf.barbican.barbican_endpoint_type = 'internal' + + endpoint_data = mock.Mock() + endpoint_data.url = 'http://localhost:9311' + + auth = mock.Mock(spec=['service_catalog']) + auth.service_catalog.endpoint_data_for.return_value = endpoint_data + + endpoint = self.key_mgr._get_barbican_endpoint(auth, mock.Mock()) + self.assertEqual(endpoint, 'http://localhost:9311') + auth.service_catalog.endpoint_data_for.assert_called_once_with( + service_type='key-manager', interface='internal', + region_name=None) + + def test_barbican_endpoint_with_region_name(self): + self.key_mgr.conf.barbican.barbican_region_name = 'regionOne' + + endpoint_data = mock.Mock() + endpoint_data.url = 'http://localhost:9311' + + auth = mock.Mock(spec=['service_catalog']) + auth.service_catalog.endpoint_data_for.return_value = endpoint_data + + endpoint = self.key_mgr._get_barbican_endpoint(auth, mock.Mock()) + self.assertEqual(endpoint, 'http://localhost:9311') + auth.service_catalog.endpoint_data_for.assert_called_once_with( + service_type='key-manager', interface='public', + region_name='regionOne') + + def test_barbican_endpoint_from_config(self): + self.key_mgr.conf.barbican.barbican_endpoint = 'http://localhost:9311' + + endpoint = self.key_mgr._get_barbican_endpoint( + mock.Mock(), mock.Mock()) + self.assertEqual(endpoint, 'http://localhost:9311') + + def test_barbican_endpoint_by_get_endpoint(self): + auth = mock.Mock(spec=['get_endppint']) + sess = mock.Mock() + auth.get_endpoint = mock.Mock(return_value='http://localhost:9311') + + endpoint = self.key_mgr._get_barbican_endpoint(auth, sess) + self.assertEqual(endpoint, 'http://localhost:9311') + auth.get_endpoint.assert_called_once_with( + sess, service_type='key-manager', interface='public', + region_name=None) + + def test_barbican_endpoint_by_get_endpoint_with_endpoint_type(self): + self.key_mgr.conf.barbican.barbican_endpoint_type = 'internal' + + auth = mock.Mock(spec=['get_endppint']) + sess = mock.Mock() + auth.get_endpoint = mock.Mock(return_value='http://localhost:9311') + + endpoint = self.key_mgr._get_barbican_endpoint(auth, sess) + self.assertEqual(endpoint, 'http://localhost:9311') + auth.get_endpoint.assert_called_once_with( + sess, service_type='key-manager', interface='internal', + region_name=None) + + def test_barbican_endpoint_by_get_endpoint_with_region_name(self): + self.key_mgr.conf.barbican.barbican_region_name = 'regionOne' + + auth = mock.Mock(spec=['get_endppint']) + sess = mock.Mock() + auth.get_endpoint = mock.Mock(return_value='http://localhost:9311') + + endpoint = self.key_mgr._get_barbican_endpoint(auth, sess) + self.assertEqual(endpoint, 'http://localhost:9311') + auth.get_endpoint.assert_called_once_with( + sess, service_type='key-manager', interface='public', + region_name='regionOne') + + def test__get_keystone_auth(self): + auth = self.key_mgr._get_keystone_auth(self.ctxt) + self.assertIsInstance(auth, identity.Token) + + def test__get_keystone_auth_service_user(self): + self.key_mgr.conf.barbican.send_service_user_token = True + auth = self.key_mgr._get_keystone_auth(self.ctxt) + self.assertIsInstance(auth, service_token.ServiceTokenAuthWrapper) def test_base_url_old_version(self): version = "v1" @@ -108,7 +209,46 @@ endpoint) self.assertEqual(endpoint + "/" + endpoint_data.api_version, base_url) auth.service_catalog.endpoint_data_for.assert_called_once_with( - service_type='key-manager') + service_type='key-manager', interface='public', + region_name=None) + + def test_base_url_service_catalog_with_endpoint_type(self): + self.key_mgr.conf.barbican.barbican_endpoint_type = 'internal' + + endpoint_data = mock.Mock() + endpoint_data.api_version = 'v321' + + auth = mock.Mock(spec=['service_catalog']) + auth.service_catalog.endpoint_data_for.return_value = endpoint_data + + endpoint = "http://localhost/key_manager" + + base_url = self.key_mgr._create_base_url(auth, + mock.Mock(), + endpoint) + self.assertEqual(endpoint + "/" + endpoint_data.api_version, base_url) + auth.service_catalog.endpoint_data_for.assert_called_once_with( + service_type='key-manager', interface='internal', + region_name=None) + + def test_base_url_service_catalog_with_region_name(self): + self.key_mgr.conf.barbican.barbican_region_name = 'regionOne' + + endpoint_data = mock.Mock() + endpoint_data.api_version = 'v321' + + auth = mock.Mock(spec=['service_catalog']) + auth.service_catalog.endpoint_data_for.return_value = endpoint_data + + endpoint = "http://localhost/key_manager" + + base_url = self.key_mgr._create_base_url(auth, + mock.Mock(), + endpoint) + self.assertEqual(endpoint + "/" + endpoint_data.api_version, base_url) + auth.service_catalog.endpoint_data_for.assert_called_once_with( + service_type='key-manager', interface='public', + region_name='regionOne') def test_base_url_raise_exception(self): auth = mock.Mock(spec=['get_discovery']) @@ -481,3 +621,16 @@ def test_list_with_invalid_object_type(self): self.assertRaises(exception.KeyManagerError, self.key_mgr.list, self.ctxt, "invalid_type") + + def test_list_options_for_discovery(self): + opts = self.key_mgr.list_options_for_discovery() + expected_sections = ['barbican', 'barbican_service_user'] + self.assertEqual(expected_sections, [section[0] for section in opts]) + barbican_opts = [opt.name for opt in opts[0][1]] + # From Castellan opts. + self.assertIn('barbican_endpoint', barbican_opts) + barbican_service_user_opts = [opt.name for opt in opts[1][1]] + # From session opts. + self.assertIn('cafile', barbican_service_user_opts) + # From auth common opts. + self.assertIn('auth_section', barbican_service_user_opts) diff --git a/lower-constraints.txt b/lower-constraints.txt deleted file mode 100644 index bc4ec34..0000000 --- a/lower-constraints.txt +++ /dev/null @@ -1,61 +0,0 @@ -appdirs==1.3.0 -asn1crypto==0.23.0 -certifi==2020.4.5.2 -cffi==1.14.0 -chardet==3.0.4 -cliff==2.8.0 -cmd2==0.8.0 -coverage==4.0 -cryptography==2.7 -debtcollector==1.2.0 -entrypoints==0.3 -extras==1.0.0 -fixtures==3.0.0 -future==0.18.2 -gitdb==0.6.4 -GitPython==1.0.1 -idna==2.5 -iso8601==0.1.11 -keystoneauth1==3.4.0 -linecache2==1.0.0 -monotonic==0.6 -mox3==0.20.0 -msgpack-python==0.4.0 -netaddr==0.7.18 -netifaces==0.10.4 -os-client-config==1.28.0 -oslo.config==6.4.0 -oslo.context==2.19.2 -oslo.i18n==3.15.3 -oslo.log==3.36.0 -oslo.serialization==2.18.0 -oslo.utils==3.33.0 -oslotest==3.2.0 -pbr==2.0.0 -pifpaf==0.10.0 -prettytable==0.7.2 -pycparser==2.18 -pyinotify==0.9.6 -pyparsing==2.1.0 -pyperclip==1.5.27 -python-barbicanclient==4.5.2 -python-dateutil==2.5.3 -python-mimeparse==1.6.0 -python-subunit==1.0.0 -pytz==2013.6 -PyYAML==3.13 -requests==2.18.0 -requestsexceptions==1.2.0 -rfc3986==0.3.1 -smmap==0.9.0 -stestr==2.0.0 -stevedore==1.20.0 -testrepository==0.0.20 -testscenarios==0.4 -testtools==2.2.0 -traceback2==1.4.0 -unittest2==1.1.0 -urllib3==1.21.1 -voluptuous==0.11.7 -wrapt==1.7.0 -xattr==0.9.2 diff --git a/releasenotes/notes/barbican-service-user-11ebbfcd33dace9d.yaml b/releasenotes/notes/barbican-service-user-11ebbfcd33dace9d.yaml new file mode 100644 index 0000000..da751a8 --- /dev/null +++ b/releasenotes/notes/barbican-service-user-11ebbfcd33dace9d.yaml @@ -0,0 +1,7 @@ +--- +features: + - | + Adds support for using a service user with the Barbican key manager. + This is enabled via ``[barbican] send_service_user_token``, with + credentials for the service user configured via keystoneauth options in the + ``[barbican_service_user]`` group. diff --git a/releasenotes/notes/use-barbican-region-name-config-option-31bec809292302b8.yaml b/releasenotes/notes/use-barbican-region-name-config-option-31bec809292302b8.yaml new file mode 100644 index 0000000..03544b0 --- /dev/null +++ b/releasenotes/notes/use-barbican-region-name-config-option-31bec809292302b8.yaml @@ -0,0 +1,7 @@ +--- +features: + - | + The new ``[barbican] barbican_region_name`` option has been added. + This parameter is used to determine the proper Barbican endpoint in + the multi-region deployment which has a different Barbican endpoint in + each region. diff --git a/releasenotes/source/index.rst b/releasenotes/source/index.rst index ba93b2e..44f563b 100644 --- a/releasenotes/source/index.rst +++ b/releasenotes/source/index.rst @@ -6,6 +6,7 @@ :maxdepth: 1 unreleased + wallaby victoria ussuri train diff --git a/releasenotes/source/wallaby.rst b/releasenotes/source/wallaby.rst new file mode 100644 index 0000000..d77b565 --- /dev/null +++ b/releasenotes/source/wallaby.rst @@ -0,0 +1,6 @@ +============================ +Wallaby Series Release Notes +============================ + +.. release-notes:: + :branch: stable/wallaby diff --git a/setup.cfg b/setup.cfg index 1c1ab27..5345a1b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,12 +1,12 @@ [metadata] name = castellan summary = Generic Key Manager interface for OpenStack -description-file = +description_file = README.rst author = OpenStack -author-email = openstack-discuss@lists.openstack.org -home-page = https://docs.openstack.org/castellan/latest/ -python-requires = >=3.6 +author_email = openstack-discuss@lists.openstack.org +home_page = https://docs.openstack.org/castellan/latest/ +python_requires = >=3.6 classifier = Environment :: OpenStack Intended Audience :: Information Technology diff --git a/tox.ini b/tox.ini index 2392ba0..f56ab61 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -minversion = 3.1.1 +minversion = 3.18.0 envlist = py3,pep8 ignore_basepython_conflict = True skipsdist = True @@ -58,12 +58,12 @@ commands= rm -rf doc/build doc/build/doctrees sphinx-build -W -b html -d doc/build/doctrees doc/source doc/build/html -whitelist_externals = rm +allowlist_externals = rm [testenv:pdf-docs] deps = {[testenv:docs]deps} envdir = {toxworkdir}/docs -whitelist_externals = +allowlist_externals = rm make commands = @@ -107,12 +107,6 @@ [hacking] import_exceptions = castellan.i18n -[testenv:lower-constraints] -deps = - -c{toxinidir}/lower-constraints.txt - -r{toxinidir}/test-requirements.txt - -r{toxinidir}/requirements.txt - [testenv:bindep] # Do not install any requirements. We want this to be fast and work even if # system dependencies are missing, since it's used to tell you what system