Add secret consumers

This change adds the ability to add or remove consumers to a
managed object to allow services to indicate which object is
associated with a specific secret.  At this time, only barbican
supports consumers.

This code cannot be merged without a corresponding release and
bump of version for the barbicanclient.

Co-Authored-By: Mauricio Harley <mharley@redhat.com>
Depends-On: https://review.opendev.org/c/openstack/requirements/+/873906
Change-Id: Ic25ac329f87db5992e32ef0b2d7d4020f37b2dee
This commit is contained in:
Grzegorz Grasza 2023-01-20 10:45:28 +01:00
parent fe10397ac0
commit bc6d87b969
7 changed files with 372 additions and 14 deletions

View File

@ -37,7 +37,6 @@ from castellan.common.objects import opaque_data as op_data
from castellan.i18n import _
from castellan.key_manager import key_manager
from barbicanclient import client as barbican_client_import
from barbicanclient import exceptions as barbican_exceptions
from oslo_utils import timeutils
@ -160,7 +159,6 @@ class BarbicanKeyManager(key_manager.KeyManager):
self._base_url = self._create_base_url(auth,
sess,
self._barbican_endpoint)
return self._barbican_client
def _get_keystone_auth(self, context):
@ -552,17 +550,20 @@ class BarbicanKeyManager(key_manager.KeyManager):
created = calendar.timegm(time_stamp)
if issubclass(secret_type, key_base_class.Key):
return secret_type(secret.algorithm,
secret.bit_length,
secret_data,
secret.name,
created,
object_id)
return secret_type(algorithm=secret.algorithm,
bit_length=secret.bit_length,
key=secret_data,
name=secret.name,
created=created,
id=object_id,
consumers=secret.consumers)
else:
# Opaque Data or Passphrase
return secret_type(secret_data,
secret.name,
created,
object_id)
name=secret.name,
created=created,
id=object_id,
consumers=secret.consumers)
def _get_secret(self, context, object_id):
"""Returns the metadata of the secret.
@ -628,7 +629,6 @@ class BarbicanKeyManager(key_manager.KeyManager):
:raises ManagedObjectNotFoundError: if the object could not be found
"""
barbican_client = self._get_barbican_client(context)
try:
secret_ref = self._create_secret_ref(managed_object_id)
barbican_client.secrets.delete(secret_ref)
@ -642,6 +642,50 @@ class BarbicanKeyManager(key_manager.KeyManager):
else:
raise exception.KeyManagerError(reason=e)
def add_consumer(self, context, managed_object_id, consumer_data):
"""Add a consumer to the specified managed object
:param context: contains information of the user and the environment
for the request (castellan/context.py)
:param managed_object_id: the UUID of the object to update
:param consumer_data: dict containing consumer data
:raises KeyManagerError: if object deletion fails
:raises ManagedObjectNotFoundError: if the object could not be found
"""
barbican_client = self._get_barbican_client(context)
try:
secret_ref = self._create_secret_ref(managed_object_id)
barbican_client.secrets.register_consumer(
secret_ref, **consumer_data)
except (barbican_exceptions.HTTPAuthError,
barbican_exceptions.HTTPClientError,
barbican_exceptions.HTTPServerError) as e:
LOG.error("Error adding consumer: %s", e)
if self._is_secret_not_found_error(e):
raise exception.ManagedObjectNotFoundError(
uuid=managed_object_id)
else:
raise exception.KeyManagerError(reason=e)
def remove_consumer(self, context, managed_object_id, consumer_data):
barbican_client = self._get_barbican_client(context)
try:
secret_ref = self._create_secret_ref(managed_object_id)
barbican_client.secrets.remove_consumer(
secret_ref, **consumer_data)
except (barbican_exceptions.HTTPAuthError,
barbican_exceptions.HTTPClientError,
barbican_exceptions.HTTPServerError) as e:
LOG.error("Error removing consumer: %s", e)
if self._is_secret_not_found_error(e):
raise exception.ManagedObjectNotFoundError(
uuid=managed_object_id)
else:
raise exception.KeyManagerError(reason=e)
def list(self, context, object_type=None, metadata_only=False):
"""Retrieves a list of managed objects that match the criteria.

View File

@ -122,6 +122,40 @@ class KeyManager(object, metaclass=abc.ABCMeta):
"""
pass
@abc.abstractmethod
def add_consumer(self, context, managed_object_id, consumer_data):
"""Add a consumer to a managed object.
Implementations should verify that the caller has permission to
add a consumer to the managed object by checking the context object
(context). A NotAuthorized exception should be raised if the caller
lacks permission.
If the specified object does not exist, then a KeyError should be
raised. Implementations should preclude users from discerning the
UUIDs of objects that belong to other users by repeatedly calling this
method. That is, objects that belong to other users should be
considered "non-existent" and completely invisible.
"""
pass
@abc.abstractmethod
def remove_consumer(self, context, managed_object_id, consumer_data):
"""Remove a consumer from a managed object.
Implementations should verify that the caller has permission to
remove a consumer to the managed object by checking the context object
(context). A NotAuthorized exception should be raised if the caller
lacks permission.
If the specified object does not exist, then a KeyError should be
raised. Implementations should preclude users from discerning the
UUIDs of objects that belong to other users by repeatedly calling this
method. That is, objects that belong to other users should be
considered "non-existent" and completely invisible.
"""
pass
def list(self, context, object_type=None, metadata_only=False):
"""Lists the managed objects given the criteria.

View File

@ -48,5 +48,11 @@ class NotImplementedKeyManager(key_manager.KeyManager):
def list(self, context, object_type=None):
raise NotImplementedError()
def delete(self, context, managed_object_id, **kwargs):
def delete(self, context, managed_object_id):
raise NotImplementedError()
def add_consumer(self, context, managed_object_id, consumer_data):
raise NotImplementedError()
def remove_consumer(self, context, managed_object_id, consumer_data):
raise NotImplementedError()

View File

@ -355,6 +355,16 @@ class VaultKeyManager(key_manager.KeyManager):
if resp.status_code == requests.codes['not_found']:
raise exception.ManagedObjectNotFoundError(uuid=key_id)
def add_consumer(self, context, managed_object_id, consumer_data):
raise NotImplementedError(
"VaultKeyManager does not implement adding consumers"
)
def remove_consumer(self, context, managed_object_id, consumer_data):
raise NotImplementedError(
"VaultKeyManager does not implement deleting consumers"
)
def list(self, context, object_type=None, metadata_only=False):
"""Lists the managed objects given the criteria."""

View File

@ -199,6 +199,22 @@ class MockKeyManager(key_manager.KeyManager):
del self.keys[managed_object_id]
def add_consumer(self, context, managed_object_id, consumer_data):
if context is None:
raise exception.Forbidden()
if managed_object_id not in self.keys:
raise exception.ManagedObjectNotFoundError()
self.keys[managed_object_id].consumers.append(consumer_data)
def remove_consumer(self, context, managed_object_id, consumer_data):
if context is None:
raise exception.Forbidden()
if managed_object_id not in self.keys:
raise exception.ManagedObjectNotFoundError()
self.keys[managed_object_id].consumers = [
c for c in self.keys[managed_object_id].consumers
if c != consumer_data]
def _generate_password(self, length, symbolgroups):
"""Generate a random password from the supplied symbol groups.

View File

@ -24,6 +24,7 @@ from keystoneauth1 import identity
from keystoneauth1 import service_token
from oslo_context import context
from oslo_utils import timeutils
from oslo_utils import uuidutils
from castellan.common import exception
from castellan.common.objects import symmetric_key as sym_key
@ -78,8 +79,13 @@ class BarbicanKeyManagerTestCase(test_key_manager.KeyManagerTestCase):
self.create = self.mock_barbican.secrets.create
self.list = self.mock_barbican.secrets.list
self.add_consumer = self.mock_barbican.secrets.add_consumer
self.remove_consumer = self.mock_barbican.secrets.remove_consumer
self.list_versions = self.mock_barbican.versions.list_versions
self.key_mgr._barbican_client = self.mock_barbican
self.key_mgr._current_context = self.ctxt
self.key_mgr._version_client = self.mock_barbican
def test_barbican_endpoint(self):
endpoint_data = mock.Mock()
@ -634,3 +640,245 @@ class BarbicanKeyManagerTestCase(test_key_manager.KeyManagerTestCase):
self.assertIn('cafile', barbican_service_user_opts)
# From auth common opts.
self.assertIn('auth_section', barbican_service_user_opts)
def _test_consumer_expects_error(
self, Error, method, ctxt, obj_ref, service="storage",
resource_type='volume', resource_id=uuidutils.generate_uuid()):
consumer_data = self._get_custom_consumer_data(
service=service, resource_type=resource_type,
resource_id=resource_id)
self.assertRaises(
Error, method, ctxt, obj_ref, consumer_data)
def _mock_list_versions_and_test_consumer_expects_error(
self, Error, method, ctxt, obj_ref, service="storage",
resource_type='volume', resource_id=uuidutils.generate_uuid()):
self._mock_list_versions()
self._test_consumer_expects_error(
Error, method, ctxt, obj_ref, service=service,
resource_type=resource_type, resource_id=resource_id)
def _mock_list_versions_and_test_add_consumer_expects_error(
self, Error, ctxt, obj_ref, side_effect=None, service="storage",
resource_type='volume', resource_id=uuidutils.generate_uuid()):
self.mock_barbican.secrets.register_consumer = mock.Mock(
side_effect=side_effect)
self._mock_list_versions_and_test_consumer_expects_error(
Error, self.key_mgr.add_consumer, ctxt,
obj_ref, service=service, resource_type=resource_type,
resource_id=resource_id)
def _mock_list_versions_and_test_remove_consumer_expects_error(
self, Error, ctxt, obj_ref, side_effect=None, service="storage",
resource_type='volume', resource_id=uuidutils.generate_uuid()):
self.mock_barbican.secrets.remove_consumer = mock.Mock(
side_effect=side_effect)
self._mock_list_versions_and_test_consumer_expects_error(
Error, self.key_mgr.remove_consumer, ctxt,
obj_ref, service=service, resource_type=resource_type,
resource_id=resource_id)
def _mock_list_versions(self):
list_versions = [{
'id': 'v1', 'status': 'CURRENT', 'min_version': '1.0',
'max_version': '1.1', 'links': []}
]
self.list_versions.return_value = list_versions
def _get_custom_consumer_data(
self, service="storage", resource_type='volume',
resource_id=uuidutils.generate_uuid()):
return {
'service': service, 'resource_type': resource_type,
'resource_id': resource_id}
def test_add_consumer_without_context_fails(self):
self.key_mgr._barbican_client = None
self._test_consumer_expects_error(
exception.Forbidden, self.key_mgr.add_consumer, None,
self.secret_ref)
def test_add_consumer_with_different_project_fails(self):
side_effect = barbican_exceptions.HTTPClientError(
"Forbidden: SecretConsumer creation attempt not allowed - "
"please review your user/project privileges")
self._mock_list_versions_and_test_add_consumer_expects_error(
exception.KeyManagerError, self.ctxt, self.secret_ref,
side_effect=side_effect)
def test_add_consumer_with_null_managed_object_id_fails(self):
self._mock_list_versions_and_test_add_consumer_expects_error(
exception.KeyManagerError, self.ctxt, None)
def test_add_consumer_with_empty_managed_object_id_fails(self):
self._mock_list_versions_and_test_add_consumer_expects_error(
exception.KeyManagerError, self.ctxt, "")
def test_add_consumer_with_invalid_managed_object_id_fails(self):
side_effect = ValueError("Secret incorrectly specified.")
self._mock_list_versions_and_test_add_consumer_expects_error(
ValueError, self.ctxt, uuidutils.generate_uuid()[:-1],
side_effect=side_effect)
def test_add_consumer_with_inexistent_managed_object_id_fails(self):
side_effect = barbican_exceptions.HTTPClientError(
"Not Found: Secret not found.", status_code=404)
self._mock_list_versions_and_test_add_consumer_expects_error(
exception.ManagedObjectNotFoundError, self.ctxt, self.secret_ref,
side_effect=side_effect)
def test_add_consumer_with_null_service_fails(self):
side_effect = barbican_exceptions.HTTPClientError(
"Bad Request: Provided object does not match schema "
"'Secret Consumer': None is not of type 'string'. Invalid "
"property: 'service'", status_code=400)
self._mock_list_versions_and_test_add_consumer_expects_error(
exception.KeyManagerError, self.ctxt, self.secret_ref,
side_effect=side_effect, service=None)
def test_add_consumer_with_empty_service_fails(self):
side_effect = barbican_exceptions.HTTPClientError(
"Bad Request: Provided object does not match schema "
"'Secret Consumer': '' is too short. Invalid property: 'service'",
status_code=400)
self._mock_list_versions_and_test_add_consumer_expects_error(
exception.KeyManagerError, self.ctxt, self.secret_ref,
side_effect=side_effect, service="")
def test_add_consumer_with_null_resource_type_fails(self):
side_effect = barbican_exceptions.HTTPClientError(
"Bad Request: Provided object does not match schema "
"'Secret Consumer': None is not of type 'string'. "
"Invalid property: 'resource_type'", status_code=400)
self._mock_list_versions_and_test_add_consumer_expects_error(
exception.KeyManagerError, self.ctxt, self.secret_ref,
side_effect=side_effect, resource_type=None)
def test_add_consumer_with_empty_resource_type_fails(self):
side_effect = barbican_exceptions.HTTPClientError(
"Bad Request: Provided object does not match schema "
"'Secret Consumer': '' is too short. Invalid property: "
"'resource_type'", status_code=400)
self._mock_list_versions_and_test_add_consumer_expects_error(
exception.KeyManagerError, self.ctxt, self.secret_ref,
side_effect=side_effect, resource_type="")
def test_add_consumer_with_null_resource_id_fails(self):
side_effect = barbican_exceptions.HTTPClientError(
"Bad Request: Provided object does not match schema "
"'Secret Consumer': None is not of type 'string'. "
"Invalid property: 'resource_id'", status_code=400)
self._mock_list_versions_and_test_add_consumer_expects_error(
exception.KeyManagerError, self.ctxt, self.secret_ref,
side_effect=side_effect, resource_id=None)
def test_add_consumer_with_empty_resource_id_fails(self):
side_effect = barbican_exceptions.HTTPClientError(
"Bad Request: Provided object does not match schema "
"'Secret Consumer': '' is too short. Invalid property: "
"'resource_id'", status_code=400)
self._mock_list_versions_and_test_add_consumer_expects_error(
exception.KeyManagerError, self.ctxt, self.secret_ref,
side_effect=side_effect, resource_id="")
def test_add_consumer_with_valid_parameters_doesnt_fail(self):
self._mock_list_versions()
self.key_mgr.add_consumer(
self.ctxt, self.secret_ref, self._get_custom_consumer_data())
def test_remove_consumer_without_context_fails(self):
self.key_mgr._barbican_client = None
self._test_consumer_expects_error(
exception.Forbidden, self.key_mgr.remove_consumer, None,
self.secret_ref)
def test_remove_consumer_with_different_project_fails(self):
side_effect = barbican_exceptions.HTTPClientError(
"Forbidden: SecretConsumer creation attempt not allowed - "
"please review your user/project privileges")
self._mock_list_versions_and_test_remove_consumer_expects_error(
exception.KeyManagerError, self.ctxt, self.secret_ref,
side_effect=side_effect)
def test_remove_consumer_with_null_managed_object_id_fails(self):
side_effect = ValueError("secret incorrectly specified.")
self._mock_list_versions_and_test_add_consumer_expects_error(
exception.KeyManagerError, self.ctxt, None,
side_effect=side_effect)
def test_remove_consumer_with_empty_managed_object_id_fails(self):
side_effect = ValueError("secret incorrectly specified.")
self._mock_list_versions_and_test_add_consumer_expects_error(
exception.KeyManagerError, self.ctxt, "", side_effect=side_effect)
def test_remove_consumer_with_invalid_managed_object_id_fails(self):
side_effect = ValueError("Secret incorrectly specified.")
self._mock_list_versions_and_test_add_consumer_expects_error(
ValueError, self.ctxt, uuidutils.generate_uuid()[:-1],
side_effect=side_effect)
def test_remove_consumer_without_registered_managed_object_id_fails(self):
side_effect = barbican_exceptions.HTTPClientError(
"Not Found: Secret not found.", status_code=404)
self._mock_list_versions_and_test_add_consumer_expects_error(
exception.ManagedObjectNotFoundError, self.ctxt, self.secret_ref,
side_effect=side_effect)
def test_remove_consumer_with_null_service_fails(self):
side_effect = barbican_exceptions.HTTPClientError(
"Bad Request: Provided object does not match schema "
"'Secret Consumer': None is not of type 'string'. Invalid "
"property: 'service'", status_code=400)
self._mock_list_versions_and_test_add_consumer_expects_error(
exception.KeyManagerError, self.ctxt, self.secret_ref,
side_effect=side_effect, service=None)
def test_remove_consumer_with_empty_service_fails(self):
side_effect = barbican_exceptions.HTTPClientError(
"Bad Request: Provided object does not match schema "
"'Secret Consumer': '' is too short. Invalid property: 'service'",
status_code=400)
self._mock_list_versions_and_test_add_consumer_expects_error(
exception.KeyManagerError, self.ctxt, self.secret_ref,
side_effect=side_effect, service="")
def test_remove_consumer_with_null_resource_type_fails(self):
side_effect = barbican_exceptions.HTTPClientError(
"Bad Request: Provided object does not match schema "
"'Secret Consumer': None is not of type 'string'. "
"Invalid property: 'resource_type'", status_code=400)
self._mock_list_versions_and_test_add_consumer_expects_error(
exception.KeyManagerError, self.ctxt, self.secret_ref,
side_effect=side_effect, resource_type=None)
def test_remove_consumer_with_empty_resource_type_fails(self):
side_effect = barbican_exceptions.HTTPClientError(
"Bad Request: Provided object does not match schema "
"'Secret Consumer': '' is too short. Invalid property: "
"'resource_type'", status_code=400)
self._mock_list_versions_and_test_add_consumer_expects_error(
exception.KeyManagerError, self.ctxt, self.secret_ref,
side_effect=side_effect, resource_type="")
def test_remove_consumer_with_null_resource_id_fails(self):
side_effect = barbican_exceptions.HTTPClientError(
"Bad Request: Provided object does not match schema "
"'Secret Consumer': None is not of type 'string'. "
"Invalid property: 'resource_id'", status_code=400)
self._mock_list_versions_and_test_add_consumer_expects_error(
exception.KeyManagerError, self.ctxt, self.secret_ref,
side_effect=side_effect, resource_id=None)
def test_remove_consumer_with_empty_resource_id_fails(self):
side_effect = barbican_exceptions.HTTPClientError(
"Bad Request: Provided object does not match schema "
"'Secret Consumer': '' is too short. Invalid property: "
"'resource_id'", status_code=400)
self._mock_list_versions_and_test_add_consumer_expects_error(
exception.KeyManagerError, self.ctxt, self.secret_ref,
side_effect=side_effect, resource_id="")
def test_remove_consumer_with_valid_parameters_doesnt_fail(self):
self._mock_list_versions()
self.key_mgr.remove_consumer(
self.ctxt, self.secret_ref, self._get_custom_consumer_data())

View File

@ -4,7 +4,7 @@
pbr!=2.1.0,>=2.0.0 # Apache-2.0
cryptography>=2.7 # BSD/Apache-2.0
python-barbicanclient>=4.5.2 # Apache-2.0
python-barbicanclient>=5.5.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