From 960159332864740cfbdaeb339ddbaae48238a6d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Douglas=20Mendiz=C3=A1bal?= <dmendiza@redhat.com> Date: Thu, 4 Nov 2021 16:57:09 -0500 Subject: [PATCH] Fix container consumers rbac policy This patch modifies the Consumer controller to enable the use of ownership information in policy checks. e.g. policies that use a target container: project_id:%(target.container.project_id) Story: 2009664 Task: 43872 Depends-On: I8698fc7a9ac849b8c24adfe824ca44dd3e42b999 Change-Id: I1724152839f0f5850f8d32d40b36d1670c0ad996 --- barbican/api/controllers/__init__.py | 25 ++-- barbican/api/controllers/acls.py | 2 + barbican/api/controllers/consumers.py | 73 +++++------ barbican/api/controllers/containers.py | 17 +-- barbican/api/controllers/orders.py | 3 +- barbican/api/controllers/quotas.py | 3 + barbican/api/controllers/secretmeta.py | 6 +- barbican/api/controllers/secrets.py | 6 +- barbican/api/controllers/secretstores.py | 3 + barbican/api/controllers/transportkeys.py | 2 + barbican/common/policies/consumers.py | 113 +++++++++++++----- barbican/tests/api/test_resources_policy.py | 67 +++++++++-- .../api/v1/functional/test_acls.py | 34 ++++-- .../fix-story-2009664-042ef282c0dd6b6a.yaml | 13 ++ 14 files changed, 245 insertions(+), 122 deletions(-) create mode 100644 releasenotes/notes/fix-story-2009664-042ef282c0dd6b6a.yaml diff --git a/barbican/api/controllers/__init__.py b/barbican/api/controllers/__init__.py index 9cc2d2220..8c9d0a5d2 100644 --- a/barbican/api/controllers/__init__.py +++ b/barbican/api/controllers/__init__.py @@ -173,8 +173,22 @@ def flatten(d, parent_key=''): class ACLMixin(object): + def __init__(self): + self.secret = None + self.container = None + def get_acl_tuple(self, req, **kwargs): - return None, None + if self.secret is not None: + entity = 'secret' + elif self.container is not None: + entity = 'container' + else: + return None, None + entity_acls = getattr(getattr(self, entity), '{}_acls'.format(entity)) + acl = self.get_acl_dict_for_user(req, entity_acls) + acl['project_id'] = getattr(self, entity).project.external_id + acl['creator_id'] = getattr(self, entity).creator_id + return entity, acl def get_acl_dict_for_user(self, req, acl_list): """Get acl operation found for token user in acl list. @@ -237,12 +251,3 @@ class ACLMixin(object): acl_dict.update(co_dict) return acl_dict - - -class SecretACLMixin(ACLMixin): - - def get_acl_tuple(self, req, **kwargs): - acl = self.get_acl_dict_for_user(req, self.secret.secret_acls) - acl['project_id'] = self.secret.project.external_id - acl['creator_id'] = self.secret.creator_id - return 'secret', acl diff --git a/barbican/api/controllers/acls.py b/barbican/api/controllers/acls.py index d65f87af2..cf9b0ceb6 100644 --- a/barbican/api/controllers/acls.py +++ b/barbican/api/controllers/acls.py @@ -47,6 +47,7 @@ class SecretACLsController(controllers.ACLMixin): """Handles SecretACL requests by a given secret id.""" def __init__(self, secret): + super().__init__() self.secret = secret self.secret_project_id = self.secret.project.external_id self.acl_repo = repo.get_secret_acl_repository() @@ -210,6 +211,7 @@ class ContainerACLsController(controllers.ACLMixin): """Handles ContainerACL requests by a given container id.""" def __init__(self, container): + super().__init__() self.container = container self.container_id = container.id self.acl_repo = repo.get_container_acl_repository() diff --git a/barbican/api/controllers/consumers.py b/barbican/api/controllers/consumers.py index ed0585c8a..037d67d6d 100644 --- a/barbican/api/controllers/consumers.py +++ b/barbican/api/controllers/consumers.py @@ -46,7 +46,9 @@ def _invalid_consumer_id(): class ContainerConsumerController(controllers.ACLMixin): """Handles Container Consumer entity retrieval and deletion requests""" - def __init__(self, consumer_id): + def __init__(self, container, consumer_id): + super().__init__() + self.container = container self.consumer_id = consumer_id self.consumer_repo = repo.get_container_consumer_repository() self.validator = validators.ContainerConsumerValidator() @@ -78,8 +80,10 @@ class ContainerConsumerController(controllers.ACLMixin): class ContainerConsumersController(controllers.ACLMixin): """Handles Container Consumer creation requests""" - def __init__(self, container_id): - self.container_id = container_id + def __init__(self, container): + super().__init__() + self.container = container + self.container_id = self.container.id self.consumer_repo = repo.get_container_consumer_repository() self.container_repo = repo.get_container_repository() self.project_repo = repo.get_project_repository() @@ -91,7 +95,8 @@ class ContainerConsumersController(controllers.ACLMixin): def _lookup(self, consumer_id, *remainder): if not utils.validate_id_is_uuid(consumer_id): _invalid_consumer_id()() - return ContainerConsumerController(consumer_id), remainder + return ContainerConsumerController(self.container, consumer_id), \ + remainder @pecan.expose(generic=True) def index(self, **kwargs): @@ -99,7 +104,7 @@ class ContainerConsumersController(controllers.ACLMixin): @index.when(method='GET', template='json') @controllers.handle_exceptions(u._('ContainerConsumers(s) retrieval')) - @controllers.enforce_rbac('consumers:get') + @controllers.enforce_rbac('container_consumers:get') def on_get(self, external_project_id, **kw): LOG.debug('Start consumers on_get ' 'for container-ID %s:', self.container_id) @@ -137,7 +142,7 @@ class ContainerConsumersController(controllers.ACLMixin): @index.when(method='POST', template='json') @controllers.handle_exceptions(u._('ContainerConsumer creation')) - @controllers.enforce_rbac('consumers:post') + @controllers.enforce_rbac('container_consumers:post') @controllers.enforce_content_types(['application/json']) def on_post(self, external_project_id, **kwargs): @@ -145,14 +150,12 @@ class ContainerConsumersController(controllers.ACLMixin): data = api.load_body(pecan.request, validator=self.validator) LOG.debug('Start on_post...%s', data) - container = self._get_container(self.container_id) - self.quota_enforcer.enforce(project) new_consumer = models.ContainerConsumerMetadatum(self.container_id, project.id, data) - self.consumer_repo.create_or_update_from(new_consumer, container) + self.consumer_repo.create_or_update_from(new_consumer, self.container) url = hrefs.convert_consumer_to_href(new_consumer.container_id) pecan.response.headers['Location'] = url @@ -160,19 +163,16 @@ class ContainerConsumersController(controllers.ACLMixin): LOG.info('Created a container consumer for project: %s', external_project_id) - return self._return_container_data(self.container_id) + return self._return_container_data() @index.when(method='DELETE', template='json') @controllers.handle_exceptions(u._('ContainerConsumer deletion')) - @controllers.enforce_rbac('consumers:delete') + @controllers.enforce_rbac('container_consumers:delete') @controllers.enforce_content_types(['application/json']) def on_delete(self, external_project_id, **kwargs): data = api.load_body(pecan.request, validator=self.validator) LOG.debug('Start on_delete...%s', data) - project = self.project_repo.find_by_external_project_id( - external_project_id, suppress_exception=True) - if not project: - _consumer_not_found() + project = res.get_or_create_project(external_project_id) consumer = self.consumer_repo.get_by_values( self.container_id, @@ -184,9 +184,8 @@ class ContainerConsumersController(controllers.ACLMixin): _consumer_not_found() LOG.debug("Found container consumer: %s", consumer) - container = self._get_container(self.container_id) owner_of_consumer = consumer.project_id == project.id - owner_of_container = container.project.external_id \ + owner_of_container = self.container.project.external_id \ == external_project_id if not owner_of_consumer and not owner_of_container: _consumer_ownership_mismatch() @@ -198,22 +197,13 @@ class ContainerConsumersController(controllers.ACLMixin): LOG.exception('Problem deleting container consumer') _consumer_not_found() - ret_data = self._return_container_data(self.container_id) + ret_data = self._return_container_data() LOG.info('Deleted a container consumer for project: %s', external_project_id) return ret_data - def _get_container(self, container_id): - container = self.container_repo.get_container_by_id( - container_id, suppress_exception=True) - if not container: - controllers.containers.container_not_found() - return container - - def _return_container_data(self, container_id): - container = self._get_container(container_id) - - dict_fields = container.to_dict_fields() + def _return_container_data(self): + dict_fields = self.container.to_dict_fields() for secret_ref in dict_fields['secret_refs']: hrefs.convert_to_hrefs(secret_ref) @@ -227,7 +217,9 @@ class ContainerConsumersController(controllers.ACLMixin): class SecretConsumerController(controllers.ACLMixin): """Handles Secret Consumer entity retrieval and deletion requests""" - def __init__(self, consumer_id): + def __init__(self, secret, consumer_id): + super().__init__() + self.secret = secret self.consumer_id = consumer_id self.consumer_repo = repo.get_secret_consumer_repository() self.validator = validators.SecretConsumerValidator() @@ -259,8 +251,10 @@ class SecretConsumerController(controllers.ACLMixin): class SecretConsumersController(controllers.ACLMixin): """Handles Secret Consumer creation requests""" - def __init__(self, secret_id): - self.secret_id = secret_id + def __init__(self, secret): + super().__init__() + self.secret = secret + self.secret_id = secret.id self.consumer_repo = repo.get_secret_consumer_repository() self.secret_repo = repo.get_secret_repository() self.project_repo = repo.get_project_repository() @@ -272,7 +266,7 @@ class SecretConsumersController(controllers.ACLMixin): def _lookup(self, consumer_id, *remainder): if not utils.validate_id_is_uuid(consumer_id): _invalid_consumer_id()() - return SecretConsumerController(consumer_id), remainder + return SecretConsumerController(self.secret, consumer_id), remainder @pecan.expose(generic=True) def index(self, **kwargs): @@ -280,7 +274,7 @@ class SecretConsumersController(controllers.ACLMixin): @index.when(method='GET', template='json') @controllers.handle_exceptions(u._('SecretConsumers(s) retrieval')) - @controllers.enforce_rbac('consumers:get') + @controllers.enforce_rbac('secret_consumers:get') def on_get(self, external_project_id, **kw): LOG.debug('Start consumers on_get ' 'for secret-ID %s:', self.secret_id) @@ -318,7 +312,7 @@ class SecretConsumersController(controllers.ACLMixin): @index.when(method='POST', template='json') @controllers.handle_exceptions(u._('SecretConsumer creation')) - @controllers.enforce_rbac('consumers:post') + @controllers.enforce_rbac('secret_consumers:post') @controllers.enforce_content_types(['application/json']) def on_post(self, external_project_id, **kwargs): @@ -326,8 +320,6 @@ class SecretConsumersController(controllers.ACLMixin): data = api.load_body(pecan.request, validator=self.validator) LOG.debug('Start on_post...%s', data) - secret = self._get_secret(self.secret_id) - self.quota_enforcer.enforce(project) new_consumer = models.SecretConsumerMetadatum( @@ -337,7 +329,7 @@ class SecretConsumersController(controllers.ACLMixin): data["resource_type"], data["resource_id"], ) - self.consumer_repo.create_or_update_from(new_consumer, secret) + self.consumer_repo.create_or_update_from(new_consumer, self.secret) url = hrefs.convert_consumer_to_href(new_consumer.secret_id) pecan.response.headers['Location'] = url @@ -349,7 +341,7 @@ class SecretConsumersController(controllers.ACLMixin): @index.when(method='DELETE', template='json') @controllers.handle_exceptions(u._('SecretConsumer deletion')) - @controllers.enforce_rbac('consumers:delete') + @controllers.enforce_rbac('secret_consumers:delete') @controllers.enforce_content_types(['application/json']) def on_delete(self, external_project_id, **kwargs): data = api.load_body(pecan.request, validator=self.validator) @@ -368,9 +360,8 @@ class SecretConsumersController(controllers.ACLMixin): _consumer_not_found() LOG.debug("Found consumer: %s", consumer) - secret = self._get_secret(self.secret_id) owner_of_consumer = consumer.project_id == project.id - owner_of_secret = secret.project.external_id \ + owner_of_secret = self.secret.project.external_id \ == external_project_id if not owner_of_consumer and not owner_of_secret: _consumer_ownership_mismatch() diff --git a/barbican/api/controllers/containers.py b/barbican/api/controllers/containers.py index bac8934c4..1550a968e 100644 --- a/barbican/api/controllers/containers.py +++ b/barbican/api/controllers/containers.py @@ -45,21 +45,16 @@ class ContainerController(controllers.ACLMixin): """Handles Container entity retrieval and deletion requests.""" def __init__(self, container): + super().__init__() self.container = container self.container_id = container.id self.consumer_repo = repo.get_container_consumer_repository() self.container_repo = repo.get_container_repository() self.validator = validators.ContainerValidator() self.consumers = consumers.ContainerConsumersController( - self.container_id) + self.container) self.acl = acls.ContainerACLsController(self.container) - def get_acl_tuple(self, req, **kwargs): - d = self.get_acl_dict_for_user(req, self.container.container_acls) - d['project_id'] = self.container.project.external_id - d['creator_id'] = self.container.creator_id - return 'container', d - @pecan.expose(generic=True, template='json') def index(self, **kwargs): pecan.abort(405) # HTTP 405 Method Not Allowed as default @@ -112,6 +107,7 @@ class ContainersController(controllers.ACLMixin): """Handles Container creation requests.""" def __init__(self): + super().__init__() self.consumer_repo = repo.get_container_consumer_repository() self.container_repo = repo.get_container_repository() self.secret_repo = repo.get_secret_repository() @@ -230,17 +226,12 @@ class ContainersSecretsController(controllers.ACLMixin): def __init__(self, container): LOG.debug('=== Creating ContainerSecretsController ===') + super().__init__() self.container = container self.container_secret_repo = repo.get_container_secret_repository() self.secret_repo = repo.get_secret_repository() self.validator = validators.ContainerSecretValidator() - def get_acl_tuple(self, req, **kwargs): - acl = self.get_acl_dict_for_user(req, self.container.container_acls) - acl['project_id'] = self.container.project.external_id - acl['creator_id'] = self.container.creator_id - return ('container', acl) - @pecan.expose(generic=True) def index(self, **kwargs): pecan.abort(405) # HTTP 405 Method Not Allowed as default diff --git a/barbican/api/controllers/orders.py b/barbican/api/controllers/orders.py index 635407ae5..977ae6455 100644 --- a/barbican/api/controllers/orders.py +++ b/barbican/api/controllers/orders.py @@ -61,6 +61,7 @@ class OrderController(controllers.ACLMixin): """Handles Order retrieval and deletion requests.""" def __init__(self, order, queue_resource=None): + super().__init__() self.order = order self.order_repo = repo.get_order_repository() self.queue = queue_resource or async_client.TaskClient() @@ -96,8 +97,8 @@ class OrdersController(controllers.ACLMixin): """Handles Order requests for Secret creation.""" def __init__(self, queue_resource=None): - LOG.debug('Creating OrdersController') + super().__init__() self.order_repo = repo.get_order_repository() self.queue = queue_resource or async_client.TaskClient() self.type_order_validator = validators.TypeOrderValidator() diff --git a/barbican/api/controllers/quotas.py b/barbican/api/controllers/quotas.py index e387da4e3..cdf70a4fe 100644 --- a/barbican/api/controllers/quotas.py +++ b/barbican/api/controllers/quotas.py @@ -37,6 +37,7 @@ class QuotasController(controllers.ACLMixin): def __init__(self): LOG.debug('=== Creating QuotasController ===') + super().__init__() self.quota_driver = quota.QuotaDriver() @pecan.expose(generic=True) @@ -59,6 +60,7 @@ class ProjectQuotasController(controllers.ACLMixin): def __init__(self, project_id): LOG.debug('=== Creating ProjectQuotasController ===') + super().__init__() self.passed_project_id = project_id self.validator = validators.ProjectQuotaValidator() self.quota_driver = quota.QuotaDriver() @@ -114,6 +116,7 @@ class ProjectsQuotasController(controllers.ACLMixin): def __init__(self): LOG.debug('=== Creating ProjectsQuotaController ===') + super().__init__() self.quota_driver = quota.QuotaDriver() @pecan.expose() diff --git a/barbican/api/controllers/secretmeta.py b/barbican/api/controllers/secretmeta.py index 8215beac7..8f31a8224 100644 --- a/barbican/api/controllers/secretmeta.py +++ b/barbican/api/controllers/secretmeta.py @@ -28,11 +28,12 @@ def _secret_metadata_not_found(): pecan.abort(404, u._('Secret metadata not found.')) -class SecretMetadataController(controllers.SecretACLMixin): +class SecretMetadataController(controllers.ACLMixin): """Handles SecretMetadata requests by a given secret id.""" def __init__(self, secret): LOG.debug('=== Creating SecretMetadataController ===') + super().__init__() self.secret = secret self.secret_project_id = self.secret.project.external_id self.secret_repo = repo.get_secret_repository() @@ -106,10 +107,11 @@ class SecretMetadataController(controllers.SecretACLMixin): return {'key': key, 'value': value} -class SecretMetadatumController(controllers.SecretACLMixin): +class SecretMetadatumController(controllers.ACLMixin): def __init__(self, secret): LOG.debug('=== Creating SecretMetadatumController ===') + super().__init__() self.user_meta_repo = repo.get_secret_user_meta_repository() self.secret = secret self.metadatum_validator = validators.NewSecretMetadatumValidator() diff --git a/barbican/api/controllers/secrets.py b/barbican/api/controllers/secrets.py index 12d6caf37..d9289b00c 100644 --- a/barbican/api/controllers/secrets.py +++ b/barbican/api/controllers/secrets.py @@ -71,13 +71,14 @@ def _request_has_twsk_but_no_transport_key_id(): 'transport key id has not been provided.')) -class SecretController(controllers.SecretACLMixin): +class SecretController(controllers.ACLMixin): """Handles Secret retrieval and deletion requests.""" def __init__(self, secret): LOG.debug('=== Creating SecretController ===') + super().__init__() self.secret = secret - self.consumers = consumers.SecretConsumersController(secret.id) + self.consumers = consumers.SecretConsumersController(secret) self.consumer_repo = repo.get_secret_consumer_repository() self.transport_key_repo = repo.get_transport_key_repository() @@ -276,6 +277,7 @@ class SecretsController(controllers.ACLMixin): def __init__(self): LOG.debug('Creating SecretsController') + super().__init__() self.validator = validators.NewSecretValidator() self.secret_repo = repo.get_secret_repository() self.quota_enforcer = quota.QuotaEnforcer('secrets', self.secret_repo) diff --git a/barbican/api/controllers/secretstores.py b/barbican/api/controllers/secretstores.py index b328568ed..fb79651be 100644 --- a/barbican/api/controllers/secretstores.py +++ b/barbican/api/controllers/secretstores.py @@ -57,6 +57,7 @@ class PreferredSecretStoreController(controllers.ACLMixin): def __init__(self, secret_store): LOG.debug('=== Creating PreferredSecretStoreController ===') + super().__init__() self.secret_store = secret_store self.proj_store_repo = repo.get_project_secret_store_repository() @@ -103,6 +104,7 @@ class SecretStoreController(controllers.ACLMixin): def __init__(self, secret_store): LOG.debug('=== Creating SecretStoreController ===') + super().__init__() self.secret_store = secret_store @pecan.expose() @@ -129,6 +131,7 @@ class SecretStoresController(controllers.ACLMixin): def __init__(self): LOG.debug('Creating SecretStoresController') + super().__init__() self.secret_stores_repo = repo.get_secret_stores_repository() self.proj_store_repo = repo.get_project_secret_store_repository() diff --git a/barbican/api/controllers/transportkeys.py b/barbican/api/controllers/transportkeys.py index 892b5e0c4..08a834b5b 100644 --- a/barbican/api/controllers/transportkeys.py +++ b/barbican/api/controllers/transportkeys.py @@ -43,6 +43,7 @@ class TransportKeyController(controllers.ACLMixin): def __init__(self, transport_key_id, transport_key_repo=None): LOG.debug('=== Creating TransportKeyController ===') + super().__init__() self.transport_key_id = transport_key_id self.repo = transport_key_repo or repo.TransportKeyRepo() @@ -83,6 +84,7 @@ class TransportKeysController(controllers.ACLMixin): def __init__(self, transport_key_repo=None): LOG.debug('Creating TransportKeyController') + super().__init__() self.repo = transport_key_repo or repo.TransportKeyRepo() self.validator = validators.NewTransportKeyValidator() diff --git a/barbican/common/policies/consumers.py b/barbican/common/policies/consumers.py index b99c7b843..3f8ce1f34 100644 --- a/barbican/common/policies/consumers.py +++ b/barbican/common/policies/consumers.py @@ -21,10 +21,20 @@ _READER = "role:reader" _MEMBER = "role:member" _ADMIN = "role:admin" _SYSTEM_ADMIN = "role:admin and system_scope:all" -_PROJECT_MEMBER = f"{_MEMBER} and project_id:%(target.container.project_id)s" -_PROJECT_ADMIN = f"{_ADMIN} and project_id:%(target.container.project_id)s" + +_SECRET_CREATOR = "user_id:%(target.secret.creator_id)s" +_SECRET_PROJECT = "project_id:%(target.secret.project_id)s" +_SECRET_MEMBER = f"{_MEMBER} and {_SECRET_PROJECT}" +_SECRET_ADMIN = f"{_ADMIN} and {_SECRET_PROJECT}" +_SECRET_ACCESS = (f"{_SECRET_CREATOR} or ({_SECRET_MEMBER} and " + f"True:%(target.secret.read_project_access)s)") + _CONTAINER_CREATOR = "user_id:%(target.container.creator_id)s" -_CONTAINER_IS_NOT_PRIVATE = "True:%(target.container.read_project_access)s" +_CONTAINER_PROJECT = "project_id:%(target.container.project_id)s" +_CONTAINER_MEMBER = f"{_MEMBER} and {_CONTAINER_PROJECT}" +_CONTAINER_ADMIN = f"{_ADMIN} and {_CONTAINER_PROJECT}" +_CONTAINER_ACCESS = (f"{_CONTAINER_CREATOR} or ({_CONTAINER_MEMBER} and " + f"True:%(target.container.read_project_access)s)") rules = [ policy.DocumentedRuleDefault( @@ -33,27 +43,23 @@ rules = [ 'rule:audit or rule:container_non_private_read or ' + 'rule:container_project_creator or ' + 'rule:container_project_admin or rule:container_acl_read' + - f" or ({_PROJECT_MEMBER} and ({_CONTAINER_CREATOR} or " + - f"{_CONTAINER_IS_NOT_PRIVATE})) or {_PROJECT_ADMIN} or " + + f" or {_CONTAINER_ACCESS} or {_CONTAINER_ADMIN} or " + f"{_SYSTEM_ADMIN}", scope_types=['project', 'system'], - description='List a specific consumer for a given container.', - operations=[ - { - 'path': '/v1/containers/{container-id}/consumers/' + - '{consumer-id}', - 'method': 'GET' - } - ] + # This API is unusable. There is no way for a user to get + # the consumer-id they would need to send a request. + description='DEPRECATED: show information for a specific consumer', + operations=[{ + 'path': '/v1/containers/{container-id}/consumers/{consumer-id}', + 'method': 'GET' + }] ), policy.DocumentedRuleDefault( - name='consumers:get', - check_str='rule:admin or rule:observer or rule:creator or ' + - 'rule:audit or rule:container_non_private_read or ' + + name='container_consumers:get', + check_str='rule:container_non_private_read or ' + 'rule:container_project_creator or ' + - 'rule:container_project_admin or rule:container_acl_read' + - f" or ({_PROJECT_MEMBER} and ({_CONTAINER_CREATOR} or " + - f"{_CONTAINER_IS_NOT_PRIVATE})) or {_PROJECT_ADMIN} or " + + 'rule:container_project_admin or rule:container_acl_read ' + + f" or {_CONTAINER_ACCESS} or {_CONTAINER_ADMIN} or " + f"{_SYSTEM_ADMIN}", scope_types=['project', 'system'], description='List a containers consumers.', @@ -65,12 +71,11 @@ rules = [ ] ), policy.DocumentedRuleDefault( - name='consumers:post', - check_str='rule:admin or rule:container_non_private_read or ' + + name='container_consumers:post', + check_str='rule:container_non_private_read or ' + 'rule:container_project_creator or ' + - 'rule:container_project_admin or rule:container_acl_read' + - f" or ({_PROJECT_MEMBER} and ({_CONTAINER_CREATOR} or " + - f"{_CONTAINER_IS_NOT_PRIVATE})) or {_PROJECT_ADMIN} or " + + 'rule:container_project_admin or rule:container_acl_read ' + + f" or {_CONTAINER_ACCESS} or {_CONTAINER_ADMIN} or " + f"{_SYSTEM_ADMIN}", scope_types=['project', 'system'], description='Creates a consumer.', @@ -82,19 +87,65 @@ rules = [ ] ), policy.DocumentedRuleDefault( - name='consumers:delete', - check_str='rule:admin or rule:container_non_private_read or ' + + name='container_consumers:delete', + check_str='rule:container_non_private_read or ' + 'rule:container_project_creator or ' + - 'rule:container_project_admin or rule:container_acl_read' + - f" or ({_PROJECT_MEMBER} and ({_CONTAINER_CREATOR} or " + - f"{_CONTAINER_IS_NOT_PRIVATE})) or {_PROJECT_ADMIN} or " + + 'rule:container_project_admin or rule:container_acl_read ' + + f" or {_CONTAINER_ACCESS} or {_CONTAINER_ADMIN} or " + f"{_SYSTEM_ADMIN}", scope_types=['project', 'system'], description='Deletes a consumer.', operations=[ { - 'path': '/v1/containers/{container-id}/consumers/' + - '{consumer-id}', + 'path': '/v1/containers/{container-id}/consumers', + 'method': 'DELETE' + } + ] + ), + policy.DocumentedRuleDefault( + name='secret_consumers:get', + check_str='rule:secret_non_private_read or ' + + 'rule:secret_project_creator or ' + + 'rule:secret_project_admin or rule:secret_acl_read ' + + f" or {_SECRET_ACCESS} or {_SECRET_ADMIN} or " + + f"{_SYSTEM_ADMIN}", + scope_types=['project', 'system'], + description='List consumers for a secret.', + operations=[ + { + 'path': '/v1/secrets/{secret-id}/consumers', + 'method': 'GET' + } + ] + ), + policy.DocumentedRuleDefault( + name='secret_consumers:post', + check_str='rule:secret_non_private_read or ' + + 'rule:secret_project_creator or ' + + 'rule:secret_project_admin or rule:secret_acl_read ' + + f" or {_SECRET_ACCESS} or {_SECRET_ADMIN} or " + + f"{_SYSTEM_ADMIN}", + scope_types=['project', 'system'], + description='Creates a consumer.', + operations=[ + { + 'path': '/v1/secrets/{secrets-id}/consumers', + 'method': 'POST' + } + ] + ), + policy.DocumentedRuleDefault( + name='secret_consumers:delete', + check_str='rule:secret_non_private_read or ' + + 'rule:secret_project_creator or ' + + 'rule:secret_project_admin or rule:secret_acl_read ' + + f" or {_SECRET_ACCESS} or {_SECRET_ADMIN} or " + + f"{_SYSTEM_ADMIN}", + scope_types=['project', 'system'], + description='Deletes a consumer.', + operations=[ + { + 'path': '/v1/secrets/{secrets-id}/consumers', 'method': 'DELETE' } ] diff --git a/barbican/tests/api/test_resources_policy.py b/barbican/tests/api/test_resources_policy.py index bf6ab224a..af984e949 100644 --- a/barbican/tests/api/test_resources_policy.py +++ b/barbican/tests/api/test_resources_policy.py @@ -1059,10 +1059,12 @@ class WhenTestingConsumersResource(BaseTestCase): self.external_project_id = '12345project' self.container_id = '12345container' + self.creator_user_id = '123456CreatorUser' # Force an error on GET calls that pass RBAC, as we are not testing # such flows in this test module. self.consumer_repo = mock.MagicMock() + self.container_repo = mock.MagicMock() get_by_container_id = mock.MagicMock(return_value=None, side_effect=self ._generate_get_error()) @@ -1072,23 +1074,34 @@ class WhenTestingConsumersResource(BaseTestCase): self.setup_container_consumer_repository_mock(self.consumer_repo) self.setup_container_repository_mock() - self.resource = ConsumersResource(container_id=self.container_id) + container = mock.MagicMock() + container.id = self.container_id + container.project.external_id = self.external_project_id + container.creator_id = self.creator_user_id + self.container_repo.get_container_by_id.return_value = container + self.setup_container_repository_mock(self.container_repo) + + self.resource = ConsumersResource(container) def test_rules_should_be_loaded(self): self.assertIsNotNone(self.policy_enforcer.rules) def test_should_pass_create_consumer(self): self._assert_pass_rbac(['admin'], self._invoke_on_post, - content_type='application/json') + content_type='application/json', + project_id=self.external_project_id) def test_should_raise_create_consumer(self): self._assert_fail_rbac([None, 'audit', 'observer', 'creator', 'bogus'], self._invoke_on_post, - content_type='application/json') + content_type='application/json', + user_id='some_other_user', + project_id='some_other_project') def test_should_pass_delete_consumer(self): self._assert_pass_rbac(['admin'], self._invoke_on_delete, - content_type='application/json') + content_type='application/json', + project_id=self.external_project_id) def test_should_raise_delete_consumer(self): self._assert_fail_rbac([None, 'audit', 'observer', 'creator', 'bogus'], @@ -1097,7 +1110,8 @@ class WhenTestingConsumersResource(BaseTestCase): def test_should_pass_get_consumers(self): self._assert_pass_rbac(['admin', 'observer', 'creator', 'audit'], self._invoke_on_get, - content_type='application/json') + content_type='application/json', + project_id=self.external_project_id) def test_should_raise_get_consumers(self): self._assert_fail_rbac([None, 'bogus'], @@ -1121,17 +1135,28 @@ class WhenTestingConsumerResource(BaseTestCase): self.external_project_id = '12345project' self.consumer_id = '12345consumer' + self.container_id = '12345container' + self.creator_user_id = '123456CreatorUser' # Force an error on GET calls that pass RBAC, as we are not testing # such flows in this test module. self.consumer_repo = mock.MagicMock() + self.container_repo = mock.MagicMock() fail_method = mock.MagicMock(return_value=None, side_effect=self._generate_get_error()) self.consumer_repo.get = fail_method self.setup_project_repository_mock() self.setup_container_consumer_repository_mock(self.consumer_repo) - self.resource = ConsumerResource(consumer_id=self.consumer_id) + + container = mock.MagicMock() + container.id = self.container_id + container.project.external_id = self.external_project_id + container.creator_id = self.creator_user_id + self.container_repo.get_container_by_id.return_value = container + self.setup_container_repository_mock(self.container_repo) + + self.resource = ConsumerResource(container, self.consumer_id) def test_rules_should_be_loaded(self): self.assertIsNotNone(self.policy_enforcer.rules) @@ -1292,6 +1317,7 @@ class WhenTestingSecretConsumersResource(BaseTestCase): self.external_project_id = '12345project' self.secret_id = '12345secret' + self.creator_user_id = '123456CreatorUser' # Force an error on GET calls that pass RBAC, as we are not testing # such flows in this test module. @@ -1301,27 +1327,35 @@ class WhenTestingSecretConsumersResource(BaseTestCase): ._generate_get_error()) self.consumer_repo.get_by_secret_id = get_by_secret_id + secret = mock.MagicMock() + secret.id = self.secret_id + secret.project.external_id = self.external_project_id + secret.creator_id = self.creator_user_id + self.setup_project_repository_mock() self.setup_secret_consumer_repository_mock(self.consumer_repo) self.setup_secret_repository_mock() - self.resource = SecretConsumersResource(secret_id=self.secret_id) + self.resource = SecretConsumersResource(secret) def test_rules_should_be_loaded(self): self.assertIsNotNone(self.policy_enforcer.rules) def test_should_pass_create_consumer(self): self._assert_pass_rbac(['admin'], self._invoke_on_post, - content_type='application/json') + content_type='application/json', + project_id=self.external_project_id) def test_should_raise_create_consumer(self): self._assert_fail_rbac([None, 'audit', 'observer', 'creator', 'bogus'], self._invoke_on_post, - content_type='application/json') + content_type='application/json', + project_id='some_other_id') def test_should_pass_delete_consumer(self): self._assert_pass_rbac(['admin'], self._invoke_on_delete, - content_type='application/json') + content_type='application/json', + project_id=self.external_project_id) def test_should_raise_delete_consumer(self): self._assert_fail_rbac([None, 'audit', 'observer', 'creator', 'bogus'], @@ -1330,12 +1364,14 @@ class WhenTestingSecretConsumersResource(BaseTestCase): def test_should_pass_get_consumers(self): self._assert_pass_rbac(['admin', 'observer', 'creator', 'audit'], self._invoke_on_get, - content_type='application/json') + content_type='application/json', + project_id=self.external_project_id) def test_should_raise_get_consumers(self): self._assert_fail_rbac([None, 'bogus'], self._invoke_on_get, - content_type='application/json') + content_type='application/json', + project_id='some_other_id') def _invoke_on_post(self): self.resource.on_post(self.req, self.resp) @@ -1354,6 +1390,7 @@ class WhenTestingSecretConsumerResource(BaseTestCase): self.external_project_id = '12345project' self.consumer_id = '12345consumer' + self.creator_user_id = '123456CreatorUser' # Force an error on GET calls that pass RBAC, as we are not testing # such flows in this test module. @@ -1362,9 +1399,13 @@ class WhenTestingSecretConsumerResource(BaseTestCase): side_effect=self._generate_get_error()) self.consumer_repo.get = fail_method + secret = mock.MagicMock() + secret.project.external_id = self.external_project_id + secret.creator_id = self.creator_user_id + self.setup_project_repository_mock() self.setup_secret_consumer_repository_mock(self.consumer_repo) - self.resource = SecretConsumerResource(consumer_id=self.consumer_id) + self.resource = SecretConsumerResource(secret, self.consumer_id) def test_rules_should_be_loaded(self): self.assertIsNotNone(self.policy_enforcer.rules) diff --git a/functionaltests/api/v1/functional/test_acls.py b/functionaltests/api/v1/functional/test_acls.py index db19ca268..e701a9a41 100644 --- a/functionaltests/api/v1/functional/test_acls.py +++ b/functionaltests/api/v1/functional/test_acls.py @@ -136,20 +136,22 @@ test_data_read_container_consumer_acl_only = { test_data_delete_container_consumer_acl_only = { 'with_admin_a': {'user': admin_a, 'expected_return': 200}, - 'with_creator_a': {'user': creator_a, 'expected_return': 403}, - 'with_observer_a': {'user': observer_a, 'expected_return': 403}, - 'with_auditor_a': {'user': auditor_a, 'expected_return': 403}, + 'with_creator_a': {'user': creator_a, 'expected_return': 200}, + 'with_observer_a': {'user': observer_a, 'expected_return': 200}, + 'with_auditor_a': {'user': auditor_a, 'expected_return': 200}, + # the consumer being deleted is owned by project a, so attempts + # to remove it with users from project b below should fail 'with_admin_b': {'user': admin_b, 'expected_return': 403}, 'with_observer_b': {'user': observer_b, 'expected_return': 403}, } test_data_create_container_consumer_acl_only = { 'with_admin_a': {'user': admin_a, 'expected_return': 200}, - 'with_creator_a': {'user': creator_a, 'expected_return': 403}, - 'with_observer_a': {'user': observer_a, 'expected_return': 403}, - 'with_auditor_a': {'user': auditor_a, 'expected_return': 403}, + 'with_creator_a': {'user': creator_a, 'expected_return': 200}, + 'with_observer_a': {'user': observer_a, 'expected_return': 200}, + 'with_auditor_a': {'user': auditor_a, 'expected_return': 200}, 'with_admin_b': {'user': admin_b, 'expected_return': 200}, - 'with_observer_b': {'user': observer_b, 'expected_return': 403}, + 'with_observer_b': {'user': observer_b, 'expected_return': 200}, } @@ -272,7 +274,14 @@ class AclTestCase(base.TestCase): @utils.parameterized_dataset(test_data_delete_container_consumer_acl_only) def test_container_acl_remove_consumer(self, user, expected_return): - """Acl access will not allow you to delete a consumer""" + """Test DELETE /v1/containers/{container-id}/consumers + + Test default policy for deleting a consumer set by admin_a + from a private container owned by creator_a. + + Each user in the data set is added to the ACL and then used + to delete the consumer set by admin_a. + """ container_ref = self.store_container(user_name=creator_a, admin=admin_a) consumer_model = get_consumer_model() @@ -298,7 +307,14 @@ class AclTestCase(base.TestCase): @utils.parameterized_dataset(test_data_create_container_consumer_acl_only) def test_container_acl_create_consumer(self, user, expected_return): - """Acl access will not allow you to add a consumer""" + """Test POST /v1/containers/{container_id}/consumers + + Test default policy for adding consumers to a container owned by + creator_a and set to private. + + Each user in the data set is added to the ACL and then used + to POST a new consumer. + """ container_ref = self.store_container(user_name=creator_a, admin=admin_a) diff --git a/releasenotes/notes/fix-story-2009664-042ef282c0dd6b6a.yaml b/releasenotes/notes/fix-story-2009664-042ef282c0dd6b6a.yaml new file mode 100644 index 000000000..090816d3f --- /dev/null +++ b/releasenotes/notes/fix-story-2009664-042ef282c0dd6b6a.yaml @@ -0,0 +1,13 @@ +--- +fixes: + - | + Fixed Story 2009664 - Fixed the Consumer controller to be able to use the + associated Container's ownership information in policy checks. +security: + - | + Part of the fix for Story 2009664 required renaming the policy for + Container Consumers from "consumers:get" to "container_consumers:get", + "consumers:post" to "container_consumers:post", and "consumers:delete" + to "container_consumers:delete". If you are using custom policies to + override the default policies you will need to update them to use the + new names.