diff --git a/patrole_tempest_plugin/rbac_exceptions.py b/patrole_tempest_plugin/rbac_exceptions.py index 6bdd7dfd..ad697b05 100644 --- a/patrole_tempest_plugin/rbac_exceptions.py +++ b/patrole_tempest_plugin/rbac_exceptions.py @@ -20,16 +20,34 @@ class BasePatroleException(exceptions.TempestException): message = "An unknown RBAC exception occurred" -class RbacMalformedResponse(BasePatroleException): - message = ("The response body is missing the expected %(attribute)s due " - "to policy enforcement failure.") +class BasePatroleResponseBodyException(BasePatroleException): + message = "Response body incomplete due to RBAC authorization failure" - def __init__(self, empty=False, **kwargs): - if empty: - self.message = ("The response body is empty due to policy " - "enforcement failure.") - kwargs = {} - super(RbacMalformedResponse, self).__init__(**kwargs) + +class RbacMissingAttributeResponseBody(BasePatroleResponseBodyException): + """Raised when a list or show action is missing an attribute following + RBAC authorization failure. + """ + message = ("The response body is missing the expected %(attribute)s due " + "to policy enforcement failure") + + +class RbacPartialResponseBody(BasePatroleResponseBodyException): + """Raised when a list action only returns a subset of the available + resources. + + For example, admin can return more resources than member for a list action. + """ + message = ("The response body only lists a subset of the available " + "resources due to partial policy enforcement failure. Response " + "body: %(body)s") + + +class RbacEmptyResponseBody(BasePatroleResponseBodyException): + """Raised when a list or show action is empty following RBAC authorization + failure. + """ + message = ("The response body is empty due to policy enforcement failure.") class RbacResourceSetupFailed(BasePatroleException): diff --git a/patrole_tempest_plugin/rbac_rule_validation.py b/patrole_tempest_plugin/rbac_rule_validation.py index 575e2c31..9ca437b4 100644 --- a/patrole_tempest_plugin/rbac_rule_validation.py +++ b/patrole_tempest_plugin/rbac_rule_validation.py @@ -198,7 +198,8 @@ def action(service, test_status = ('Error, %s' % (msg)) LOG.error(msg) except (expected_exception, - rbac_exceptions.RbacMalformedResponse) as actual_exception: + rbac_exceptions.BasePatroleResponseBodyException) \ + as actual_exception: caught_exception = actual_exception test_status = 'Denied' diff --git a/patrole_tempest_plugin/tests/api/compute/test_flavor_access_rbac.py b/patrole_tempest_plugin/tests/api/compute/test_flavor_access_rbac.py index 317c1ad8..8d4d70f2 100644 --- a/patrole_tempest_plugin/tests/api/compute/test_flavor_access_rbac.py +++ b/patrole_tempest_plugin/tests/api/compute/test_flavor_access_rbac.py @@ -50,7 +50,7 @@ class FlavorAccessRbacTest(rbac_base.BaseV2ComputeRbacTest): expected_attr = 'os-flavor-access:is_public' if expected_attr not in body: - raise rbac_exceptions.RbacMalformedResponse( + raise rbac_exceptions.RbacMissingAttributeResponseBody( attribute=expected_attr) @testtools.skipIf(CONF.policy_feature_enabled.removed_nova_policies_stein, @@ -71,7 +71,7 @@ class FlavorAccessRbacTest(rbac_base.BaseV2ComputeRbacTest): # If the `expected_attr` was not found in any flavor, then policy # enforcement failed. if not public_flavors: - raise rbac_exceptions.RbacMalformedResponse( + raise rbac_exceptions.RbacMissingAttributeResponseBody( attribute=expected_attr) @decorators.idempotent_id('39cb5c8f-9990-436f-9282-fc76a41d9bac') diff --git a/patrole_tempest_plugin/tests/api/compute/test_flavor_rxtx_rbac.py b/patrole_tempest_plugin/tests/api/compute/test_flavor_rxtx_rbac.py index 0748e67e..cbb2e197 100644 --- a/patrole_tempest_plugin/tests/api/compute/test_flavor_rxtx_rbac.py +++ b/patrole_tempest_plugin/tests/api/compute/test_flavor_rxtx_rbac.py @@ -45,7 +45,7 @@ class FlavorRxtxRbacTest(rbac_base.BaseV2ComputeRbacTest): with self.rbac_utils.override_role(self): result = self.flavors_client.list_flavors(detail=True)['flavors'] if 'rxtx_factor' not in result[0]: - raise rbac_exceptions.RbacMalformedResponse( + raise rbac_exceptions.RbacMissingAttributeResponseBody( attribute='rxtx_factor') @testtools.skipIf(CONF.policy_feature_enabled.removed_nova_policies_stein, @@ -59,5 +59,5 @@ class FlavorRxtxRbacTest(rbac_base.BaseV2ComputeRbacTest): result = self.flavors_client.show_flavor( CONF.compute.flavor_ref)['flavor'] if 'rxtx_factor' not in result: - raise rbac_exceptions.RbacMalformedResponse( + raise rbac_exceptions.RbacMissingAttributeResponseBody( attribute='rxtx_factor') diff --git a/patrole_tempest_plugin/tests/api/compute/test_images_rbac.py b/patrole_tempest_plugin/tests/api/compute/test_images_rbac.py index f6c1b67d..e16222c8 100644 --- a/patrole_tempest_plugin/tests/api/compute/test_images_rbac.py +++ b/patrole_tempest_plugin/tests/api/compute/test_images_rbac.py @@ -294,7 +294,7 @@ class ImageSizeRbacTest(rbac_base.BaseV2ComputeRbacTest): expected_attr = 'OS-EXT-IMG-SIZE:size' if expected_attr not in body: - raise rbac_exceptions.RbacMalformedResponse( + raise rbac_exceptions.RbacMissingAttributeResponseBody( attribute=expected_attr) @testtools.skipIf(CONF.policy_feature_enabled.removed_nova_policies_stein, @@ -310,5 +310,5 @@ class ImageSizeRbacTest(rbac_base.BaseV2ComputeRbacTest): expected_attr = 'OS-EXT-IMG-SIZE:size' if expected_attr not in body[0]: - raise rbac_exceptions.RbacMalformedResponse( + raise rbac_exceptions.RbacMissingAttributeResponseBody( attribute=expected_attr) diff --git a/patrole_tempest_plugin/tests/api/compute/test_server_actions_rbac.py b/patrole_tempest_plugin/tests/api/compute/test_server_actions_rbac.py index a64bd202..0ff6ebe4 100644 --- a/patrole_tempest_plugin/tests/api/compute/test_server_actions_rbac.py +++ b/patrole_tempest_plugin/tests/api/compute/test_server_actions_rbac.py @@ -403,5 +403,5 @@ class ServerActionsV216RbacTest(rbac_base.BaseV2ComputeRbacTest): server = self.servers_client.show_server(self.server_id)['server'] if 'host_status' not in server: - raise rbac_exceptions.RbacMalformedResponse( + raise rbac_exceptions.RbacMissingAttributeResponseBody( attribute='host_status') diff --git a/patrole_tempest_plugin/tests/api/compute/test_server_misc_policy_actions_rbac.py b/patrole_tempest_plugin/tests/api/compute/test_server_misc_policy_actions_rbac.py index 88bea251..64e1300a 100644 --- a/patrole_tempest_plugin/tests/api/compute/test_server_misc_policy_actions_rbac.py +++ b/patrole_tempest_plugin/tests/api/compute/test_server_misc_policy_actions_rbac.py @@ -143,7 +143,7 @@ class MiscPolicyActionsRbacTest(rbac_base.BaseV2ComputeRbacTest): expected_attr = 'config_drive' # If the first server contains "config_drive", then all the others do. if expected_attr not in body[0]: - raise rbac_exceptions.RbacMalformedResponse( + raise rbac_exceptions.RbacMissingAttributeResponseBody( attribute=expected_attr) @testtools.skipIf(CONF.policy_feature_enabled.removed_nova_policies_stein, @@ -159,7 +159,7 @@ class MiscPolicyActionsRbacTest(rbac_base.BaseV2ComputeRbacTest): body = self.servers_client.show_server(self.server['id'])['server'] expected_attr = 'config_drive' if expected_attr not in body: - raise rbac_exceptions.RbacMalformedResponse( + raise rbac_exceptions.RbacMissingAttributeResponseBody( attribute=expected_attr) @utils.requires_ext(extension='os-deferred-delete', service='compute') @@ -188,7 +188,7 @@ class MiscPolicyActionsRbacTest(rbac_base.BaseV2ComputeRbacTest): body = self.servers_client.list_servers(detail=True)['servers'] # If the first server contains `expected_attr`, then all the others do. if expected_attr not in body[0]: - raise rbac_exceptions.RbacMalformedResponse( + raise rbac_exceptions.RbacMissingAttributeResponseBody( attribute=expected_attr) @testtools.skipIf(CONF.policy_feature_enabled.removed_nova_policies_stein, @@ -205,7 +205,7 @@ class MiscPolicyActionsRbacTest(rbac_base.BaseV2ComputeRbacTest): with self.rbac_utils.override_role(self): body = self.servers_client.show_server(self.server['id'])['server'] if expected_attr not in body: - raise rbac_exceptions.RbacMalformedResponse( + raise rbac_exceptions.RbacMissingAttributeResponseBody( attribute=expected_attr) @testtools.skipIf(CONF.policy_feature_enabled.removed_nova_policies_stein, @@ -229,7 +229,7 @@ class MiscPolicyActionsRbacTest(rbac_base.BaseV2ComputeRbacTest): for attr in ('host', 'instance_name'): whole_attr = 'OS-EXT-SRV-ATTR:%s' % attr if whole_attr not in body[0]: - raise rbac_exceptions.RbacMalformedResponse( + raise rbac_exceptions.RbacMissingAttributeResponseBody( attribute=whole_attr) @testtools.skipIf(CONF.policy_feature_enabled.removed_nova_policies_stein, @@ -253,7 +253,7 @@ class MiscPolicyActionsRbacTest(rbac_base.BaseV2ComputeRbacTest): for attr in ('host', 'instance_name'): whole_attr = 'OS-EXT-SRV-ATTR:%s' % attr if whole_attr not in body: - raise rbac_exceptions.RbacMalformedResponse( + raise rbac_exceptions.RbacMissingAttributeResponseBody( attribute=whole_attr) @testtools.skipIf(CONF.policy_feature_enabled.removed_nova_policies_stein, @@ -272,7 +272,7 @@ class MiscPolicyActionsRbacTest(rbac_base.BaseV2ComputeRbacTest): 'OS-EXT-STS:power_state') for attr in expected_attrs: if attr not in body[0]: - raise rbac_exceptions.RbacMalformedResponse( + raise rbac_exceptions.RbacMissingAttributeResponseBody( attribute=attr) @testtools.skipIf(CONF.policy_feature_enabled.removed_nova_policies_stein, @@ -291,7 +291,7 @@ class MiscPolicyActionsRbacTest(rbac_base.BaseV2ComputeRbacTest): 'OS-EXT-STS:power_state') for attr in expected_attrs: if attr not in body: - raise rbac_exceptions.RbacMalformedResponse( + raise rbac_exceptions.RbacMissingAttributeResponseBody( attribute=attr) @testtools.skipIf(CONF.policy_feature_enabled.removed_nova_policies_stein, @@ -310,7 +310,7 @@ class MiscPolicyActionsRbacTest(rbac_base.BaseV2ComputeRbacTest): with self.rbac_utils.override_role(self): body = self.servers_client.list_servers(detail=True)['servers'] if expected_attr not in body[0]: - raise rbac_exceptions.RbacMalformedResponse( + raise rbac_exceptions.RbacMissingAttributeResponseBody( attribute=expected_attr) @testtools.skipIf(CONF.policy_feature_enabled.removed_nova_policies_stein, @@ -329,7 +329,7 @@ class MiscPolicyActionsRbacTest(rbac_base.BaseV2ComputeRbacTest): with self.rbac_utils.override_role(self): body = self.servers_client.show_server(self.server['id'])['server'] if expected_attr not in body: - raise rbac_exceptions.RbacMalformedResponse( + raise rbac_exceptions.RbacMissingAttributeResponseBody( attribute=expected_attr) @utils.requires_ext(extension='os-instance-actions', service='compute') @@ -360,12 +360,12 @@ class MiscPolicyActionsRbacTest(rbac_base.BaseV2ComputeRbacTest): self.server['id'], request_id)['instanceAction'] if 'events' not in instance_action: - raise rbac_exceptions.RbacMalformedResponse( + raise rbac_exceptions.RbacMissingAttributeResponseBody( attribute='events') # Microversion 2.51+ returns 'events' always, but not 'traceback'. If # 'traceback' is also present then policy enforcement passed. if 'traceback' not in instance_action['events'][0]: - raise rbac_exceptions.RbacMalformedResponse( + raise rbac_exceptions.RbacMissingAttributeResponseBody( attribute='events.traceback') @testtools.skipIf(CONF.policy_feature_enabled.removed_nova_policies_stein, @@ -379,7 +379,7 @@ class MiscPolicyActionsRbacTest(rbac_base.BaseV2ComputeRbacTest): result = self.servers_client.show_server(self.server['id'])[ 'server'] if 'key_name' not in result: - raise rbac_exceptions.RbacMalformedResponse( + raise rbac_exceptions.RbacMissingAttributeResponseBody( attribute='key_name') @testtools.skipIf(CONF.policy_feature_enabled.removed_nova_policies_stein, @@ -392,7 +392,7 @@ class MiscPolicyActionsRbacTest(rbac_base.BaseV2ComputeRbacTest): with self.rbac_utils.override_role(self): result = self.servers_client.list_servers(detail=True)['servers'] if 'key_name' not in result[0]: - raise rbac_exceptions.RbacMalformedResponse( + raise rbac_exceptions.RbacMissingAttributeResponseBody( attribute='key_name') @rbac_rule_validation.action( @@ -514,7 +514,7 @@ class MiscPolicyActionsRbacTest(rbac_base.BaseV2ComputeRbacTest): body = self.servers_client.show_server(self.server['id'])['server'] for expected_attr in expected_attrs: if expected_attr not in body: - raise rbac_exceptions.RbacMalformedResponse( + raise rbac_exceptions.RbacMissingAttributeResponseBody( attribute=expected_attr) @utils.requires_ext(extension='os-simple-tenant-usage', service='compute') diff --git a/patrole_tempest_plugin/tests/api/network/test_network_segments_rbac.py b/patrole_tempest_plugin/tests/api/network/test_network_segments_rbac.py index c985111a..b4499700 100644 --- a/patrole_tempest_plugin/tests/api/network/test_network_segments_rbac.py +++ b/patrole_tempest_plugin/tests/api/network/test_network_segments_rbac.py @@ -118,4 +118,4 @@ class NetworkSegmentsRbacTest(base.BaseNetworkRbacTest): LOG.info("NotFound or Forbidden exception are not thrown when " "role doesn't have access to the endpoint. Instead, " "the response will have an empty network body.") - raise rbac_exceptions.RbacMalformedResponse(empty=True) + raise rbac_exceptions.RbacEmptyResponseBody() diff --git a/patrole_tempest_plugin/tests/api/network/test_networks_rbac.py b/patrole_tempest_plugin/tests/api/network/test_networks_rbac.py index 96ba3780..b39489af 100644 --- a/patrole_tempest_plugin/tests/api/network/test_networks_rbac.py +++ b/patrole_tempest_plugin/tests/api/network/test_networks_rbac.py @@ -363,7 +363,7 @@ class NetworksRbacTest(base.BaseNetworkRbacTest): self.network['id'], **kwargs)['network'] if len(retrieved_network) == 0: - raise rbac_exceptions.RbacMalformedResponse(empty=True) + raise rbac_exceptions.RbacEmptyResponseBody() @utils.requires_ext(extension='provider', service='network') @rbac_rule_validation.action(service="neutron", @@ -384,7 +384,7 @@ class NetworksRbacTest(base.BaseNetworkRbacTest): self.network['id'], **kwargs)['network'] if len(retrieved_network) == 0: - raise rbac_exceptions.RbacMalformedResponse(empty=True) + raise rbac_exceptions.RbacEmptyResponseBody() @utils.requires_ext(extension='provider', service='network') @rbac_rule_validation.action( @@ -406,7 +406,7 @@ class NetworksRbacTest(base.BaseNetworkRbacTest): self.network['id'], **kwargs)['network'] if len(retrieved_network) == 0: - raise rbac_exceptions.RbacMalformedResponse(empty=True) + raise rbac_exceptions.RbacEmptyResponseBody() @utils.requires_ext(extension='provider', service='network') @rbac_rule_validation.action( @@ -428,7 +428,7 @@ class NetworksRbacTest(base.BaseNetworkRbacTest): self.network['id'], **kwargs)['network'] if len(retrieved_network) == 0: - raise rbac_exceptions.RbacMalformedResponse(empty=True) + raise rbac_exceptions.RbacEmptyResponseBody() @rbac_rule_validation.action(service="neutron", rules=["get_network", "delete_network"], diff --git a/patrole_tempest_plugin/tests/api/network/test_ports_rbac.py b/patrole_tempest_plugin/tests/api/network/test_ports_rbac.py index b65bd73c..dd3537f8 100644 --- a/patrole_tempest_plugin/tests/api/network/test_ports_rbac.py +++ b/patrole_tempest_plugin/tests/api/network/test_ports_rbac.py @@ -183,7 +183,7 @@ class PortsRbacTest(base.BaseNetworkRbacTest): # Rather than throwing a 403, the field is not present, so raise exc. if fields[0] not in retrieved_port: - raise rbac_exceptions.RbacMalformedResponse( + raise rbac_exceptions.RbacMissingAttributeResponseBody( attribute='binding:vif_type') @utils.requires_ext(extension='binding', service='network') @@ -203,7 +203,7 @@ class PortsRbacTest(base.BaseNetworkRbacTest): # Rather than throwing a 403, the field is not present, so raise exc. if fields[0] not in retrieved_port: - raise rbac_exceptions.RbacMalformedResponse( + raise rbac_exceptions.RbacMissingAttributeResponseBody( attribute='binding:vif_details') @utils.requires_ext(extension='binding', service='network') @@ -226,7 +226,7 @@ class PortsRbacTest(base.BaseNetworkRbacTest): # Rather than throwing a 403, the field is not present, so raise exc. if fields[0] not in retrieved_port: - raise rbac_exceptions.RbacMalformedResponse( + raise rbac_exceptions.RbacMissingAttributeResponseBody( attribute='binding:host_id') @utils.requires_ext(extension='binding', service='network') @@ -250,7 +250,7 @@ class PortsRbacTest(base.BaseNetworkRbacTest): # Rather than throwing a 403, the field is not present, so raise exc. if fields[0] not in retrieved_port: - raise rbac_exceptions.RbacMalformedResponse( + raise rbac_exceptions.RbacMissingAttributeResponseBody( attribute='binding:profile') @rbac_rule_validation.action(service="neutron", diff --git a/patrole_tempest_plugin/tests/api/network/test_routers_rbac.py b/patrole_tempest_plugin/tests/api/network/test_routers_rbac.py index f850a3eb..399ad47c 100644 --- a/patrole_tempest_plugin/tests/api/network/test_routers_rbac.py +++ b/patrole_tempest_plugin/tests/api/network/test_routers_rbac.py @@ -179,7 +179,7 @@ class RouterRbacTest(base.BaseNetworkRbacTest): # Rather than throwing a 403, the field is not present, so raise exc. if 'distributed' not in retrieved_fields: - raise rbac_exceptions.RbacMalformedResponse( + raise rbac_exceptions.RbacMissingAttributeResponseBody( attribute='distributed') @decorators.idempotent_id('defc502c-4159-4824-b4d9-3cdcc39015b2') @@ -201,7 +201,7 @@ class RouterRbacTest(base.BaseNetworkRbacTest): # Rather than throwing a 403, the field is not present, so raise exc. if 'ha' not in retrieved_fields: - raise rbac_exceptions.RbacMalformedResponse( + raise rbac_exceptions.RbacMissingAttributeResponseBody( attribute='ha') @rbac_rule_validation.action(service="neutron", diff --git a/patrole_tempest_plugin/tests/api/network/test_security_groups_rbac.py b/patrole_tempest_plugin/tests/api/network/test_security_groups_rbac.py index 9112bf65..e9fa018d 100644 --- a/patrole_tempest_plugin/tests/api/network/test_security_groups_rbac.py +++ b/patrole_tempest_plugin/tests/api/network/test_security_groups_rbac.py @@ -126,7 +126,7 @@ class SecGroupRbacTest(base.BaseNetworkRbacTest): # Neutron may return an empty list if access is denied. if not security_groups['security_groups']: - raise rbac_exceptions.RbacMalformedResponse(empty=True) + raise rbac_exceptions.RbacEmptyResponseBody() @rbac_rule_validation.action(service="neutron", rules=["create_security_group_rule"]) @@ -170,4 +170,4 @@ class SecGroupRbacTest(base.BaseNetworkRbacTest): # Neutron may return an empty list if access is denied. if not security_rules['security_group_rules']: - raise rbac_exceptions.RbacMalformedResponse(empty=True) + raise rbac_exceptions.RbacEmptyResponseBody() diff --git a/patrole_tempest_plugin/tests/api/network/test_subnets_rbac.py b/patrole_tempest_plugin/tests/api/network/test_subnets_rbac.py index 9a5ebe49..8fe157aa 100644 --- a/patrole_tempest_plugin/tests/api/network/test_subnets_rbac.py +++ b/patrole_tempest_plugin/tests/api/network/test_subnets_rbac.py @@ -73,7 +73,7 @@ class SubnetsRbacTest(base.BaseNetworkRbacTest): # Neutron may return an empty list if access is denied. if not subnets['subnets']: - raise rbac_exceptions.RbacMalformedResponse(empty=True) + raise rbac_exceptions.RbacEmptyResponseBody() @decorators.idempotent_id('f36cd821-dd22-4bd0-b43d-110fc4b553eb') @rbac_rule_validation.action(service="neutron", diff --git a/patrole_tempest_plugin/tests/api/volume/test_groups_rbac.py b/patrole_tempest_plugin/tests/api/volume/test_groups_rbac.py index c117d23a..730e349d 100644 --- a/patrole_tempest_plugin/tests/api/volume/test_groups_rbac.py +++ b/patrole_tempest_plugin/tests/api/volume/test_groups_rbac.py @@ -208,7 +208,7 @@ class GroupTypesV3RbacTest(rbac_base.BaseVolumeRbacTest): group_type = self.create_group_type(ignore_notfound=True) if 'group_specs' not in group_type: - raise rbac_exceptions.RbacMalformedResponse( + raise rbac_exceptions.RbacMissingAttributeResponseBody( attribute='group_specs') @decorators.idempotent_id('8d9e2831-24c3-47b7-a76a-2e563287f12f') @@ -221,5 +221,5 @@ class GroupTypesV3RbacTest(rbac_base.BaseVolumeRbacTest): resp_body = self.group_types_client.show_group_type( group_type['id'])['group_type'] if 'group_specs' not in resp_body: - raise rbac_exceptions.RbacMalformedResponse( + raise rbac_exceptions.RbacMissingAttributeResponseBody( attribute='group_specs') diff --git a/patrole_tempest_plugin/tests/api/volume/test_limits_rbac.py b/patrole_tempest_plugin/tests/api/volume/test_limits_rbac.py index 3127d83f..2bd09925 100644 --- a/patrole_tempest_plugin/tests/api/volume/test_limits_rbac.py +++ b/patrole_tempest_plugin/tests/api/volume/test_limits_rbac.py @@ -51,4 +51,5 @@ class LimitsV3RbacTest(rbac_base.BaseVolumeRbacTest): 'limits']['absolute'] for key in expected_keys: if key not in absolute_limits: - raise rbac_exceptions.RbacMalformedResponse(attribute=key) + raise rbac_exceptions.RbacMissingAttributeResponseBody( + attribute=key) diff --git a/patrole_tempest_plugin/tests/api/volume/test_volume_metadata_rbac.py b/patrole_tempest_plugin/tests/api/volume/test_volume_metadata_rbac.py index 6c2c84da..7e0044d2 100644 --- a/patrole_tempest_plugin/tests/api/volume/test_volume_metadata_rbac.py +++ b/patrole_tempest_plugin/tests/api/volume/test_volume_metadata_rbac.py @@ -111,7 +111,7 @@ class VolumeMetadataV3RbacTest(rbac_base.BaseVolumeRbacTest): 'volumes'] expected_attr = 'volume_image_metadata' if expected_attr not in resp_body[0]: - raise rbac_exceptions.RbacMalformedResponse( + raise rbac_exceptions.RbacMissingAttributeResponseBody( attribute=expected_attr) @decorators.idempotent_id('53f94d52-0dd5-42cf-a3a4-59b35150b3d5') @@ -129,7 +129,7 @@ class VolumeMetadataV3RbacTest(rbac_base.BaseVolumeRbacTest): 'volume'] expected_attr = 'volume_image_metadata' if expected_attr not in resp_body: - raise rbac_exceptions.RbacMalformedResponse( + raise rbac_exceptions.RbacMissingAttributeResponseBody( attribute=expected_attr) @decorators.idempotent_id('a9d9e825-5ea3-42e6-96f3-7ac4e97b2ed0') diff --git a/patrole_tempest_plugin/tests/api/volume/test_volumes_backup_rbac.py b/patrole_tempest_plugin/tests/api/volume/test_volumes_backup_rbac.py index bf22341d..0efeb335 100644 --- a/patrole_tempest_plugin/tests/api/volume/test_volumes_backup_rbac.py +++ b/patrole_tempest_plugin/tests/api/volume/test_volumes_backup_rbac.py @@ -210,7 +210,7 @@ class VolumesBackupsV318RbacTest(rbac_base.BaseVolumeRbacTest): # Show backup API attempts to inject the attribute below into the # response body but only if policy enforcement succeeds. if self.expected_attr not in body: - raise rbac_exceptions.RbacMalformedResponse( + raise rbac_exceptions.RbacMissingAttributeResponseBody( attribute=self.expected_attr) @decorators.idempotent_id('aa40b7c0-5974-48be-8cbc-e23cc61c4c68') @@ -221,7 +221,7 @@ class VolumesBackupsV318RbacTest(rbac_base.BaseVolumeRbacTest): body = self.backups_client.list_backups(detail=True)['backups'] if self.expected_attr not in body[0]: - raise rbac_exceptions.RbacMalformedResponse( + raise rbac_exceptions.RbacMissingAttributeResponseBody( attribute=self.expected_attr) diff --git a/patrole_tempest_plugin/tests/api/volume/test_volumes_snapshots_rbac.py b/patrole_tempest_plugin/tests/api/volume/test_volumes_snapshots_rbac.py index 40469a24..55adf1ae 100644 --- a/patrole_tempest_plugin/tests/api/volume/test_volumes_snapshots_rbac.py +++ b/patrole_tempest_plugin/tests/api/volume/test_volumes_snapshots_rbac.py @@ -76,7 +76,7 @@ class VolumesSnapshotV3RbacTest(rbac_base.BaseVolumeRbacTest): self.snapshot['id'])['snapshot'] for expected_attr in expected_attrs: if expected_attr not in resp: - raise rbac_exceptions.RbacMalformedResponse( + raise rbac_exceptions.RbacMissingAttributeResponseBody( attribute=expected_attr) @rbac_rule_validation.action(service="cinder", @@ -136,5 +136,5 @@ class VolumesSnapshotV3RbacTest(rbac_base.BaseVolumeRbacTest): resp = self._list_by_param_values(with_detail=True, **params) for expected_attr in expected_attrs: if expected_attr not in resp[0]: - raise rbac_exceptions.RbacMalformedResponse( + raise rbac_exceptions.RbacMissingAttributeResponseBody( attribute=expected_attr) diff --git a/patrole_tempest_plugin/tests/unit/test_rbac_rule_validation.py b/patrole_tempest_plugin/tests/unit/test_rbac_rule_validation.py index 1531df1b..9e547b8c 100644 --- a/patrole_tempest_plugin/tests/unit/test_rbac_rule_validation.py +++ b/patrole_tempest_plugin/tests/unit/test_rbac_rule_validation.py @@ -46,8 +46,9 @@ class BaseRBACRuleValidationTest(base.TestCase): project_id=mock.sentinel.project_id) setattr(self.mock_test_args.os_primary, 'credentials', mock_creds) + self.test_roles = ['member'] self.useFixture( - patrole_fixtures.ConfPatcher(rbac_test_roles=['member'], + patrole_fixtures.ConfPatcher(rbac_test_roles=self.test_roles, group='patrole')) # Disable patrole log for unit tests. self.useFixture( @@ -69,9 +70,10 @@ class BaseRBACMultiRoleRuleValidationTest(base.TestCase): project_id=mock.sentinel.project_id) setattr(self.mock_test_args.os_primary, 'credentials', mock_creds) + self.test_roles = ['member', 'anotherrole'] self.useFixture( - patrole_fixtures.ConfPatcher( - rbac_test_roles=['member', 'anotherrole'], group='patrole')) + patrole_fixtures.ConfPatcher(rbac_test_roles=self.test_roles, + group='patrole')) # Disable patrole log for unit tests. self.useFixture( patrole_fixtures.ConfPatcher(enable_reporting=False, @@ -150,43 +152,66 @@ class RBACRuleValidationTest(BaseRBACRuleValidationTest): @mock.patch.object(rbac_rv, 'LOG', autospec=True) @mock.patch.object(rbac_rv, 'policy_authority', autospec=True) - def test_rule_validation_rbac_malformed_response_positive( + def test_rule_validation_rbac_failed_response_body_positive( self, mock_authority, mock_log): - """Test RbacMalformedResponse error is thrown without permission - passes. + """Test BasePatroleResponseBodyException error is thrown without + permission passes. - Positive test case: if RbacMalformedResponse is thrown and the user is - not allowed to perform the action, then this is a success. + Positive test case: if subclass of BasePatroleResponseBodyException is + thrown and the user is not allowed to perform the action, then this is + a success. """ mock_authority.PolicyAuthority.return_value.allowed.return_value =\ False - @rbac_rv.action(mock.sentinel.service, rules=[mock.sentinel.action]) - def test_policy(*args): - raise rbac_exceptions.RbacMalformedResponse() + def _do_test(exception_cls, **kwargs): + @rbac_rv.action(mock.sentinel.service, + rules=[mock.sentinel.action]) + def test_policy(*args): + raise exception_cls(**kwargs) - mock_log.error.assert_not_called() + mock_log.error.assert_not_called() + mock_log.warning.assert_not_called() + + _do_test(rbac_exceptions.RbacMissingAttributeResponseBody, + attribute=mock.sentinel.attr) + _do_test(rbac_exceptions.RbacPartialResponseBody, + body=mock.sentinel.body) + _do_test(rbac_exceptions.RbacEmptyResponseBody) @mock.patch.object(rbac_rv, 'LOG', autospec=True) @mock.patch.object(rbac_rv, 'policy_authority', autospec=True) - def test_rule_validation_rbac_malformed_response_negative( + def test_rule_validation_soft_authorization_exceptions( self, mock_authority, mock_log): - """Test RbacMalformedResponse error is thrown with permission fails. + """Test RbacUnderPermissionException error is thrown when any of the + soft authorization-related exceptions are raised by a test. - Negative test case: if RbacMalformedResponse is thrown and the user is - allowed to perform the action, then this is an expected failure. + Negative test case: if subclass of BasePatroleResponseBodyException is + thrown and the user is allowed to perform the action, then this is an + expected failure. """ mock_authority.PolicyAuthority.return_value.allowed.return_value = True - @rbac_rv.action(mock.sentinel.service, rules=[mock.sentinel.action]) - def test_policy(*args): - raise rbac_exceptions.RbacMalformedResponse() + def _do_test(exception_cls, **kwargs): + @rbac_rv.action(mock.sentinel.service, + rules=[mock.sentinel.action]) + def test_policy(*args): + raise exception_cls(**kwargs) - test_re = ("User with roles \['member'\] was not allowed to perform " - "the following actions: \[%s\]. " % (mock.sentinel.action)) - self.assertRaisesRegex(rbac_exceptions.RbacUnderPermissionException, - test_re, test_policy, self.mock_test_args) - self.assertRegex(mock_log.error.mock_calls[0][1][0], test_re) + test_re = (".*User with roles \[%s\] was not allowed to " + "perform the following actions: \[%s\].*" % ( + ', '.join("'%s'" % r for r in self.test_roles), + mock.sentinel.action)) + self.assertRaisesRegex( + rbac_exceptions.RbacUnderPermissionException, test_re, + test_policy, self.mock_test_args) + self.assertRegex(mock_log.error.mock_calls[0][1][0], test_re) + + _do_test(rbac_exceptions.RbacMissingAttributeResponseBody, + attribute=mock.sentinel.attr) + _do_test(rbac_exceptions.RbacPartialResponseBody, + body=mock.sentinel.body) + _do_test(rbac_exceptions.RbacEmptyResponseBody) @mock.patch.object(rbac_rv, 'LOG', autospec=True) @mock.patch.object(rbac_rv, 'policy_authority', autospec=True) @@ -397,28 +422,6 @@ class RBACMultiRoleRuleValidationTest(BaseRBACMultiRoleRuleValidationTest, self.mock_test_args) self.assertRegex(mock_log.error.mock_calls[0][1][0], test_re) - @mock.patch.object(rbac_rv, 'LOG', autospec=True) - @mock.patch.object(rbac_rv, 'policy_authority', autospec=True) - def test_rule_validation_rbac_malformed_response_negative( - self, mock_authority, mock_log): - """Test RbacMalformedResponse error is thrown with permission fails. - - Negative test case: if RbacMalformedResponse is thrown and the user is - allowed to perform the action, then this is an expected failure. - """ - mock_authority.PolicyAuthority.return_value.allowed.return_value = True - - @rbac_rv.action(mock.sentinel.service, rules=[mock.sentinel.action]) - def test_policy(*args): - raise rbac_exceptions.RbacMalformedResponse() - - test_re = ("User with roles \['member', 'anotherrole'\] was not " - "allowed to perform the following actions: \[%s\]. " % - (mock.sentinel.action)) - self.assertRaisesRegex(rbac_exceptions.RbacUnderPermissionException, - test_re, test_policy, self.mock_test_args) - self.assertRegex(mock_log.error.mock_calls[0][1][0], test_re) - @mock.patch.object(rbac_rv, 'LOG', autospec=True) @mock.patch.object(rbac_rv, 'policy_authority', autospec=True) def test_expect_not_found_and_raise_not_found(self, mock_authority, @@ -960,7 +963,7 @@ class RBACOverrideRoleValidationTest(BaseRBACRuleValidationTest): def test_rule_validation_override_role_patrole_exception_ignored( self, mock_authority): """Test success case where Patrole exception is raised (which is - valid in case of e.g. RbacMalformedException) after override_role + valid in case of e.g. RbacPartialResponseBody) after override_role passes. """ mock_authority.PolicyAuthority.return_value.allowed.return_value =\ diff --git a/releasenotes/notes/break-up-rbac-malformed-exception-into-discrete-exceptions-92aedb99d0a13f58.yaml b/releasenotes/notes/break-up-rbac-malformed-exception-into-discrete-exceptions-92aedb99d0a13f58.yaml new file mode 100644 index 00000000..0a93b643 --- /dev/null +++ b/releasenotes/notes/break-up-rbac-malformed-exception-into-discrete-exceptions-92aedb99d0a13f58.yaml @@ -0,0 +1,25 @@ +--- +features: + - | + The exception class ``RbacMalformedException`` has been broken up into the + following discrete exceptions: + + * ``RbacMissingAttributeResponseBody`` - incomplete means that the + response body (for show or list) is missing certain attributes + * ``RbacPartialResponseBody`` - partial means that a list response + only returned a subset of the possible results available. + * ``RbacEmptyResponseBody`` - empty means that the show or list + response body is entirely empty + + Each of the exception classes above deals with a different type of failure + related to a soft authorization failure. This means that, rather than a + 403 error code getting returned by the server, the response body is + incomplete in some way. +upgrade: + - | + The exception class ``RbacMalformedException`` has been removed. Use one + of the following exception classes instead: + + * ``RbacMissingAttributeResponseBody`` + * ``RbacPartialResponseBody`` + * ``RbacEmptyResponseBody``