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: 43873

Depends-On: I8698fc7a9ac849b8c24adfe824ca44dd3e42b999
Change-Id: I1724152839f0f5850f8d32d40b36d1670c0ad996
(cherry picked from commit 9601593328)
This commit is contained in:
Douglas Mendizábal 2021-11-04 16:57:09 -05:00
parent 382b5086a2
commit 6a5ab85f41
14 changed files with 245 additions and 122 deletions

View File

@ -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

View File

@ -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()

View File

@ -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()

View File

@ -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

View File

@ -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()

View File

@ -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()

View File

@ -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()

View File

@ -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)

View File

@ -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()

View File

@ -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()

View File

@ -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'
}
]

View File

@ -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)

View File

@ -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)

View File

@ -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.