Merge "Improve test coverage for flavor_access nova policies"
diff --git a/doc/source/rbac_validation.rst b/doc/source/rbac_validation.rst
index ccaf3c8..a3cd7e6 100644
--- a/doc/source/rbac_validation.rst
+++ b/doc/source/rbac_validation.rst
@@ -7,7 +7,7 @@
 Overview
 --------
 
-RBAC Testing Validation is broken up into 3 stages:
+RBAC testing validation is broken up into 3 stages:
 
   1. "Expected" stage. Determine whether the test should be able to succeed
      or fail based on the test role defined by ``[patrole] rbac_test_role``)
@@ -41,11 +41,20 @@
 
 .. automodule:: patrole_tempest_plugin.rbac_rule_validation
    :members:
+   :private-members:
 
 ---------------------------
 The Policy Authority Module
 ---------------------------
 
+Module called by the "RBAC Rule Validation Module" to verify whether the test
+role is allowed to execute a policy action by querying ``oslo.policy`` with
+required test data. The result is used by the "RBAC Rule Validation Module" as
+the `expected` result.
+
+This module is only called for calculating the `expected` result if
+``[patrole] test_custom_requirements`` is ``False``.
+
 Using the Policy Authority Module, policy verification is performed by:
 
 1. Pooling together the default `in-code` policy rules.
diff --git a/patrole_tempest_plugin/policy_authority.py b/patrole_tempest_plugin/policy_authority.py
index af227c4..d2d07c0 100644
--- a/patrole_tempest_plugin/policy_authority.py
+++ b/patrole_tempest_plugin/policy_authority.py
@@ -32,37 +32,72 @@
 
 
 class PolicyAuthority(RbacAuthority):
-    """A class for parsing policy rules into lists of allowed roles.
-
-    RBAC testing requires that each rule in a policy file be broken up into
-    the roles that constitute it. This class automates that process.
-
-    The list of roles per rule can be reverse-engineered by checking, for
-    each role, whether a given rule is allowed using oslo policy.
-    """
+    """A class that uses ``oslo.policy`` for validating RBAC."""
 
     def __init__(self, project_id, user_id, service, extra_target_data=None):
-        """Initialization of Rbac Policy Parser.
+        """Initialization of Policy Authority class.
 
-        Parses a policy file to create a dictionary, mapping policy actions to
-        roles. If a policy file does not exist, checks whether the policy file
-        is registered as a namespace under oslo.policy.policies. Nova, for
-        example, doesn't use a policy.json file by default; its policy is
-        implemented in code and registered as 'nova' under
-        oslo.policy.policies.
+        Validates whether a test role can perform a policy action by querying
+        ``oslo.policy`` with necessary test data.
 
-        If the policy file is not found in either place, raises an exception.
+        If a policy file does not exist, checks whether the policy file is
+        registered as a namespace under "oslo.policy.policies". Nova, for
+        example, doesn't use a policy file by default; its policies are
+        implemented in code and registered as "nova" under
+        "oslo.policy.policies".
 
-        Additionally, if the policy file exists in both code and as a
-        policy.json (for example, by creating a custom nova policy.json file),
-        the custom policy file over the default policy implementation is
-        prioritized.
+        If the policy file is not found in either code or in a policy file,
+        then an exception is raised.
+
+        Additionally, if a custom policy file exists along with the default
+        policy in code implementation, the custom policy is prioritized.
 
         :param uuid project_id: project_id of object performing API call
         :param uuid user_id: user_id of object performing API call
         :param string service: service of the policy file
         :param dict extra_target_data: dictionary containing additional object
             data needed by oslo.policy to validate generic checks
+
+        Example:
+
+        .. code-block:: python
+
+            # Below is the default policy implementation in code, defined in
+            # a service like Nova.
+            test_policies = [
+                policy.DocumentedRuleDefault(
+                    'service:test_rule',
+                    base.RULE_ADMIN_OR_OWNER,
+                    "This is a description for a test policy",
+                    [
+                        {
+                            'method': 'POST',
+                            'path': '/path/to/test/resource'
+                        }
+                    ]),
+                    'service:another_test_rule',
+                    base.RULE_ADMIN_OR_OWNER,
+                    "This is a description for another test policy",
+                    [
+                        {
+                            'method': 'GET',
+                            'path': '/path/to/test/resource'
+                        }
+                    ]),
+            ]
+
+        .. code-block:: yaml
+
+            # Below is the custom override of the default policy in a YAML
+            # policy file. Note that the default rule is "rule:admin_or_owner"
+            # and the custom rule is "rule:admin_api". The `PolicyAuthority`
+            # class will use the "rule:admin_api" definition for this policy
+            # action.
+            "service:test_rule" : "rule:admin_api"
+
+            # Note below that no override is provided for
+            # "service:another_test_rule", which means that the default policy
+            # rule is used: "rule:admin_or_owner".
         """
 
         if extra_target_data is None:
@@ -108,9 +143,10 @@
 
     @classmethod
     def discover_policy_files(cls):
-        # Dynamically discover the policy file for each service in
-        # ``cls.available_services``. Pick the first ``candidate_path`` found
-        # out of the potential paths in ``CONF.patrole.custom_policy_files``.
+        """Dynamically discover the policy file for each service in
+        ``cls.available_services``. Pick the first candidate path found
+        out of the potential paths in ``[patrole] custom_policy_files``.
+        """
         if not hasattr(cls, 'policy_files'):
             cls.policy_files = {}
             for service in cls.available_services:
@@ -120,6 +156,11 @@
                                                     candidate_path % service)
 
     def allowed(self, rule_name, role):
+        """Checks if a given rule in a policy is allowed with given role.
+
+        :param string rule_name: Rule to be checked using ``oslo.policy``.
+        :param bool is_admin: Whether admin context is used.
+        """
         is_admin_context = self._is_admin_context(role)
         is_allowed = self._allowed(
             access=self._get_access_token(role),
@@ -220,13 +261,11 @@
         return access_token
 
     def _allowed(self, access, apply_rule, is_admin=False):
-        """Checks if a given rule in a policy is allowed with given access.
+        """Checks if a given rule in a policy is allowed with given ``access``.
 
-        Adapted from oslo_policy.shell.
-
-        :param access: type dict: dictionary from ``_get_access_token``
-        :param apply_rule: type string: rule to be checked
-        :param is_admin: type bool: whether admin context is used
+        :param dict access: Dictionary from ``_get_access_token``.
+        :param string apply_rule: Rule to be checked using ``oslo.policy``.
+        :param bool is_admin: Whether admin context is used.
         """
         access_data = copy.copy(access['token'])
         access_data['roles'] = [role['name'] for role in access_data['roles']]
diff --git a/patrole_tempest_plugin/rbac_rule_validation.py b/patrole_tempest_plugin/rbac_rule_validation.py
index 69274b3..540d006 100644
--- a/patrole_tempest_plugin/rbac_rule_validation.py
+++ b/patrole_tempest_plugin/rbac_rule_validation.py
@@ -38,7 +38,7 @@
 
 def action(service, rule='', admin_only=False, expected_error_code=403,
            extra_target_data=None):
-    """A decorator for verifying policy enforcement.
+    """A decorator for verifying OpenStack policy enforcement.
 
     A decorator which allows for positive and negative RBAC testing. Given:
 
@@ -50,7 +50,7 @@
     API call that enforces the ``rule``.
 
     This decorator should only be applied to an instance or subclass of
-        `tempest.base.BaseTestCase`.
+        ``tempest.test.BaseTestCase``.
 
     The result from ``_is_authorized`` is used to determine the *expected*
     test result. The *actual* test result is determined by running the
@@ -68,7 +68,7 @@
 
     As such, negative and positive testing can be applied using this decorator.
 
-    :param service: A OpenStack service. Examples: "nova" or "neutron".
+    :param service: An OpenStack service. Examples: "nova" or "neutron".
     :param rule: A policy action defined in a policy.json file (or in
         code).
 
@@ -76,11 +76,10 @@
 
             Patrole currently only supports custom JSON policy files.
 
-    :param admin_only: Skips over `oslo.policy` check because the policy action
-        defined by `rule` is not enforced by the service's policy
+    :param admin_only: Skips over ``oslo.policy`` check because the policy
+        action defined by ``rule`` is not enforced by the service's policy
         enforcement engine. For example, Keystone v2 performs an admin check
-        for most of its endpoints. If True, `rule` is effectively
-        ignored.
+        for most of its endpoints. If True, ``rule`` is effectively ignored.
     :param expected_error_code: Overrides default value of 403 (Forbidden)
         with endpoint-specific error code. Currently only supports 403 and 404.
         Support for 404 is needed because some services, like Neutron,
@@ -89,11 +88,11 @@
         .. warning::
 
             A 404 should not be provided *unless* the endpoint masks a
-            `Forbidden` exception as a `Not Found` exception.
+            ``Forbidden`` exception as a ``NotFound`` exception.
 
-    :param extra_target_data: Dictionary, keyed with `oslo.policy` generic
+    :param extra_target_data: Dictionary, keyed with ``oslo.policy`` generic
         check names, whose values are string literals that reference nested
-        `tempest.base.BaseTestCase` attributes. Used by `oslo.policy` for
+        ``tempest.test.BaseTestCase`` attributes. Used by ``oslo.policy`` for
         performing matching against attributes that are sent along with the API
         calls. Example::
 
@@ -102,8 +101,7 @@
                 "os_alt.auth_provider.credentials.user_id"
             })
 
-    :raises NotFound: If `service` is invalid or if Tempest credentials cannot
-        be found.
+    :raises NotFound: If ``service`` is invalid.
     :raises Forbidden: For item (2) above.
     :raises RbacOverPermission: For item (3) above.
 
@@ -112,7 +110,7 @@
         @rbac_rule_validation.action(
             service="nova", rule="os_compute_api:os-agents")
         def test_list_agents_rbac(self):
-            # The call to ``switch_role`` is mandatory.
+            # The call to `switch_role` is mandatory.
             self.rbac_utils.switch_role(self, toggle_rbac_role=True)
             self.agents_client.list_agents()
     """
@@ -162,7 +160,7 @@
             except Exception as e:
                 exc_info = sys.exc_info()
                 error_details = exc_info[1].__str__()
-                msg = ("An unexpected exception has occurred during test: %s, "
+                msg = ("An unexpected exception has occurred during test: %s. "
                        "Exception was: %s"
                        % (test_func.__name__, error_details))
                 test_status = ('Error, %s' % (error_details))
@@ -194,20 +192,19 @@
 def _is_authorized(test_obj, service, rule, extra_target_data, admin_only):
     """Validates whether current RBAC role has permission to do policy action.
 
-    :param test_obj: An instance or subclass of `tempest.base.BaseTestCase`.
+    :param test_obj: An instance or subclass of ``tempest.test.BaseTestCase``.
     :param service: The OpenStack service that enforces ``rule``.
     :param rule: The name of the policy action. Examples include
         "identity:create_user" or "os_compute_api:os-agents".
-    :param extra_target_data: Dictionary, keyed with `oslo.policy` generic
+    :param extra_target_data: Dictionary, keyed with ``oslo.policy`` generic
         check names, whose values are string literals that reference nested
-        `tempest.base.BaseTestCase` attributes. Used by `oslo.policy` for
+        ``tempest.test.BaseTestCase`` attributes. Used by ``oslo.policy`` for
         performing matching against attributes that are sent along with the API
         calls.
-    :param admin_only: Skips over `oslo.policy` check because the policy action
-        defined by `rule` is not enforced by the service's policy
+    :param admin_only: Skips over ``oslo.policy`` check because the policy
+        action defined by ``rule`` is not enforced by the service's policy
         enforcement engine. For example, Keystone v2 performs an admin check
-        for most of its endpoints. If True, `rule` is effectively
-        ignored.
+        for most of its endpoints. If True, ``rule`` is effectively ignored.
 
     :returns: True if the current RBAC role can perform the policy action,
         else False.
@@ -268,14 +265,15 @@
     """Dynamically calculate the expected exception to be caught.
 
     Dynamically calculate the expected exception to be caught by the test case.
-    Only `Forbidden` and `NotFound` exceptions are permitted. `NotFound` is
-    supported because Neutron, for security reasons, masks `Forbidden`
-    exceptions as `NotFound` exceptions.
+    Only ``Forbidden`` and ``NotFound`` exceptions are permitted. ``NotFound``
+    is supported because Neutron, for security reasons, masks ``Forbidden``
+    exceptions as ``NotFound`` exceptions.
 
     :param expected_error_code: the integer representation of the expected
-        exception to be caught. Must be contained in `_SUPPORTED_ERROR_CODES`.
+        exception to be caught. Must be contained in
+        ``_SUPPORTED_ERROR_CODES``.
     :returns: tuple of the exception type corresponding to
-        `expected_error_code` and a message explaining that a non-Forbidden
+        ``expected_error_code`` and a message explaining that a non-Forbidden
         exception was expected, if applicable.
     """
     expected_exception = None
@@ -304,7 +302,8 @@
 
     Before being formatted, "extra_target_data" is a dictionary that maps a
     policy string like "trust.trustor_user_id" to a nested list of
-    `tempest.base.BaseTestCase` attributes. For example, the attribute list in:
+    ``tempest.test.BaseTestCase`` attributes. For example, the attribute list
+    in:
 
         "trust.trustor_user_id": "os.auth_provider.credentials.user_id"
 
@@ -313,14 +312,14 @@
 
         "trust.trustor_user_id": "the user_id of the `os_primary` credential"
 
-    :param test_obj: An instance or subclass of `tempest.base.BaseTestCase`.
-    :param extra_target_data: Dictionary, keyed with `oslo.policy` generic
+    :param test_obj: An instance or subclass of ``tempest.test.BaseTestCase``.
+    :param extra_target_data: Dictionary, keyed with ``oslo.policy`` generic
         check names, whose values are string literals that reference nested
-        `tempest.base.BaseTestCase` attributes. Used by `oslo.policy` for
+        ``tempest.test.BaseTestCase`` attributes. Used by ``oslo.policy`` for
         performing matching against attributes that are sent along with the API
         calls.
     :returns: Dictionary containing additional object data needed by
-        `oslo.policy` to validate generic checks.
+        ``oslo.policy`` to validate generic checks.
     """
     attr_value = test_obj
     formatted_target_data = {}
diff --git a/patrole_tempest_plugin/tests/api/volume/rbac_base.py b/patrole_tempest_plugin/tests/api/volume/rbac_base.py
index c43c552..6db364e 100644
--- a/patrole_tempest_plugin/tests/api/volume/rbac_base.py
+++ b/patrole_tempest_plugin/tests/api/volume/rbac_base.py
@@ -35,14 +35,8 @@
         super(BaseVolumeRbacTest, cls).setup_clients()
         cls.rbac_utils = rbac_utils.RbacUtils(cls)
 
-        version_checker = {
-            2: [cls.os_primary.volume_hosts_v2_client,
-                cls.os_primary.volume_types_v2_client],
-            3: [cls.os_primary.volume_hosts_v2_client,
-                cls.os_primary.volume_types_v2_client]
-        }
-        cls.volume_hosts_client, cls.volume_types_client = \
-            version_checker[cls._api_version]
+        cls.volume_hosts_client = cls.os_primary.volume_hosts_v2_client
+        cls.volume_types_client = cls.os_primary.volume_types_v2_client
         cls.groups_client = cls.os_primary.groups_v3_client
         cls.group_types_client = cls.os_primary.group_types_v3_client
 
diff --git a/patrole_tempest_plugin/tests/api/volume/test_qos_rbac.py b/patrole_tempest_plugin/tests/api/volume/test_qos_rbac.py
index 7d9ec0f..2327de8 100644
--- a/patrole_tempest_plugin/tests/api/volume/test_qos_rbac.py
+++ b/patrole_tempest_plugin/tests/api/volume/test_qos_rbac.py
@@ -146,3 +146,7 @@
         self.qos_client.disassociate_all_qos(qos['id'])
         waiters.wait_for_qos_operations(self.admin_qos_client, qos['id'],
                                         'disassociate-all')
+
+
+class VolumeQOSV3RbacTest(VolumeQOSRbacTest):
+    _api_version = 3
diff --git a/patrole_tempest_plugin/tests/api/volume/test_volume_basic_crud_rbac.py b/patrole_tempest_plugin/tests/api/volume/test_volume_basic_crud_rbac.py
index d780de7..3f5227e 100644
--- a/patrole_tempest_plugin/tests/api/volume/test_volume_basic_crud_rbac.py
+++ b/patrole_tempest_plugin/tests/api/volume/test_volume_basic_crud_rbac.py
@@ -20,11 +20,11 @@
 from patrole_tempest_plugin.tests.api.volume import rbac_base
 
 
-class VolumesV2BasicCrudRbacTest(rbac_base.BaseVolumeRbacTest):
+class VolumesBasicCrudRbacTest(rbac_base.BaseVolumeRbacTest):
 
     @classmethod
     def resource_setup(cls):
-        super(VolumesV2BasicCrudRbacTest, cls).resource_setup()
+        super(VolumesBasicCrudRbacTest, cls).resource_setup()
         cls.volume = cls.create_volume()
 
     @rbac_rule_validation.action(service="cinder",
@@ -72,5 +72,5 @@
         self.volumes_client.list_volumes(detail=True)
 
 
-class VolumesV3BasicCrudRbacTest(VolumesV2BasicCrudRbacTest):
+class VolumesBasicCrudV3RbacTest(VolumesBasicCrudRbacTest):
     _api_version = 3
diff --git a/patrole_tempest_plugin/tests/api/volume/test_volume_hosts_rbac.py b/patrole_tempest_plugin/tests/api/volume/test_volume_hosts_rbac.py
index 18a2768..ee0a0be 100644
--- a/patrole_tempest_plugin/tests/api/volume/test_volume_hosts_rbac.py
+++ b/patrole_tempest_plugin/tests/api/volume/test_volume_hosts_rbac.py
@@ -27,3 +27,19 @@
     def test_list_hosts(self):
         self.rbac_utils.switch_role(self, toggle_rbac_role=True)
         self.volume_hosts_client.list_hosts()
+
+    @decorators.idempotent_id('9ddf321e-788f-4787-b8cc-dfa59e264143')
+    @rbac_rule_validation.action(service="cinder",
+                                 rule="volume_extension:hosts")
+    def test_show_host(self):
+        hosts = self.volume_hosts_client.list_hosts()['hosts']
+        host_names = [host['host_name'] for host in hosts]
+        self.assertNotEmpty(host_names, "No available volume host was found, "
+                                        "all hosts found were: %s" % hosts)
+
+        self.rbac_utils.switch_role(self, toggle_rbac_role=True)
+        self.volume_hosts_client.show_host(host_names[0])
+
+
+class VolumeHostsV3RbacTest(VolumeHostsRbacTest):
+    _api_version = 3
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 8c4185b..f9114a8 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
@@ -103,3 +103,7 @@
         self.rbac_utils.switch_role(self, toggle_rbac_role=True)
         self.volumes_client.update_volume_image_metadata(
             self.volume['id'], image_id=self.image_id)
+
+
+class VolumeMetadataV3RbacTest(VolumeMetadataRbacTest):
+    _api_version = 3
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 7eb1cf0..7ca3d9f 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
@@ -22,6 +22,7 @@
 from tempest.lib.common.utils import test_utils
 from tempest.lib import decorators
 
+from patrole_tempest_plugin import rbac_exceptions
 from patrole_tempest_plugin import rbac_rule_validation
 from patrole_tempest_plugin.tests.api.volume import rbac_base
 
@@ -32,10 +33,6 @@
 
     credentials = ['primary', 'admin']
 
-    def setUp(self):
-        super(VolumesBackupsRbacTest, self).setUp()
-        self.volume = self.create_volume()
-
     @classmethod
     def skip_checks(cls):
         super(VolumesBackupsRbacTest, cls).skip_checks()
@@ -47,6 +44,11 @@
         super(VolumesBackupsRbacTest, cls).setup_clients()
         cls.admin_backups_client = cls.os_admin.backups_v2_client
 
+    @classmethod
+    def resource_setup(cls):
+        super(VolumesBackupsRbacTest, cls).resource_setup()
+        cls.volume = cls.create_volume()
+
     def _decode_url(self, backup_url):
         return json.loads(base64.decode_as_text(backup_url))
 
@@ -73,6 +75,7 @@
     @decorators.idempotent_id('abd92bdd-b0fb-4dc4-9cfc-de9e968f8c8a')
     def test_show_backup(self):
         backup = self.create_backup(volume_id=self.volume['id'])
+
         self.rbac_utils.switch_role(self, toggle_rbac_role=True)
         self.backups_client.show_backup(backup['id'])
 
@@ -96,8 +99,7 @@
         service="cinder",
         rule="volume_extension:backup_admin_actions:reset_status")
     def test_reset_backup_status(self):
-        volume = self.create_volume()
-        backup = self.create_backup(volume_id=volume['id'])
+        backup = self.create_backup(volume_id=self.volume['id'])
 
         self.rbac_utils.switch_role(self, toggle_rbac_role=True)
         self.backups_client.reset_backup_status(backup_id=backup['id'],
@@ -135,7 +137,7 @@
         self.rbac_utils.switch_role(self, toggle_rbac_role=True)
         self.backups_client.delete_backup(backup['id'])
         # Wait for deletion so error isn't thrown during clean up.
-        self.backups_client.wait_for_resource_deletion(backup['id'])
+        self.admin_backups_client.wait_for_resource_deletion(backup['id'])
 
     @decorators.attr(type='slow')
     @rbac_rule_validation.action(service="cinder",
@@ -143,6 +145,7 @@
     @decorators.idempotent_id('e984ec8d-e8eb-485c-98bc-f1856020303c')
     def test_export_backup(self):
         backup = self.create_backup(volume_id=self.volume['id'])
+
         self.rbac_utils.switch_role(self, toggle_rbac_role=True)
         self.backups_client.export_backup(backup['id'])['backup-record']
 
@@ -167,3 +170,34 @@
 
 class VolumesBackupsV3RbacTest(VolumesBackupsRbacTest):
     _api_version = 3
+
+
+class VolumesBackupsV318RbacTest(rbac_base.BaseVolumeRbacTest):
+    _api_version = 3
+    # The minimum microversion for showing 'os-backup-project-attr:project_id'
+    # is 3.18.
+    min_microversion = '3.18'
+    max_microversion = 'latest'
+
+    @classmethod
+    def skip_checks(cls):
+        super(VolumesBackupsV318RbacTest, cls).skip_checks()
+        if not CONF.volume_feature_enabled.backup:
+            raise cls.skipException("Cinder backup feature disabled")
+
+    @decorators.idempotent_id('69801485-d5be-4e75-bbb4-168d50b5a8c2')
+    @rbac_rule_validation.action(service="cinder",
+                                 rule="backup:backup_project_attribute")
+    def test_show_backup_project_attribute(self):
+        volume = self.create_volume()
+        backup = self.create_backup(volume_id=volume['id'])
+        expected_attr = 'os-backup-project-attr:project_id'
+
+        self.rbac_utils.switch_role(self, toggle_rbac_role=True)
+        body = self.backups_client.show_backup(backup['id'])['backup']
+
+        # Show backup API attempts to inject the attribute below into the
+        # response body but only if policy enforcement succeeds.
+        if expected_attr not in body:
+            raise rbac_exceptions.RbacMalformedResponse(
+                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 94a2306..afadb43 100644
--- a/patrole_tempest_plugin/tests/unit/test_rbac_rule_validation.py
+++ b/patrole_tempest_plugin/tests/unit/test_rbac_rule_validation.py
@@ -13,35 +13,38 @@
 #    under the License.
 
 import mock
+import testtools
 
-from tempest import config
 from tempest.lib import exceptions
+from tempest import manager
 from tempest import test
 from tempest.tests import base
 
 from patrole_tempest_plugin import rbac_exceptions
 from patrole_tempest_plugin import rbac_rule_validation as rbac_rv
-
-CONF = config.CONF
+from patrole_tempest_plugin import rbac_utils
+from patrole_tempest_plugin.tests.unit import fixtures
 
 
 class RBACRuleValidationTest(base.TestCase):
 
     def setUp(self):
         super(RBACRuleValidationTest, self).setUp()
-        self.mock_args = mock.Mock(spec=test.BaseTestCase)
-        self.mock_args.os_primary = mock.Mock()
-        self.mock_args.rbac_utils = mock.Mock()
-        self.mock_args.os_primary.credentials.project_id = \
-            mock.sentinel.project_id
-        self.mock_args.os_primary.credentials.user_id = \
-            mock.sentinel.user_id
+        self.mock_test_args = mock.Mock(spec=test.BaseTestCase)
+        self.mock_test_args.os_primary = mock.Mock(spec=manager.Manager)
+        self.mock_test_args.rbac_utils = mock.Mock(
+            spec_set=rbac_utils.RbacUtils)
 
-        CONF.set_override('rbac_test_role', 'Member', group='patrole')
-        self.addCleanup(CONF.clear_override, 'rbac_test_role', group='patrole')
+        # Setup credentials for mock client manager.
+        mock_creds = mock.Mock(user_id=mock.sentinel.user_id,
+                               project_id=mock.sentinel.project_id)
+        setattr(self.mock_test_args.os_primary, 'credentials', mock_creds)
 
-        self.mock_rbaclog = mock.patch.object(
-            rbac_rv.RBACLOG, 'info', autospec=False).start()
+        self.useFixture(
+            fixtures.ConfPatcher(rbac_test_role='Member', group='patrole'))
+
+        # Mock the RBAC log so that it is not written to for any unit tests.
+        mock.patch.object(rbac_rv.RBACLOG, 'info').start()
 
     @mock.patch.object(rbac_rv, 'LOG', autospec=True)
     @mock.patch.object(rbac_rv, 'policy_authority', autospec=True)
@@ -51,17 +54,13 @@
 
         Positive test case success scenario.
         """
-        decorator = rbac_rv.action(mock.sentinel.service, mock.sentinel.action)
+        mock_authority.PolicyAuthority.return_value.allowed.return_value = True
 
-        mock_function = mock.Mock(__name__='foo')
-        wrapper = decorator(mock_function)
+        @rbac_rv.action(mock.sentinel.service, mock.sentinel.action)
+        def test_policy(*args):
+            pass
 
-        mock_authority.PolicyAuthority.return_value.allowed\
-            .return_value = True
-
-        result = wrapper(self.mock_args)
-
-        self.assertIsNone(result)
+        test_policy(self.mock_test_args)
         mock_log.warning.assert_not_called()
         mock_log.error.assert_not_called()
 
@@ -73,18 +72,14 @@
 
         Negative test case success scenario.
         """
-        decorator = rbac_rv.action(mock.sentinel.service, mock.sentinel.action)
+        mock_authority.PolicyAuthority.return_value.allowed.return_value =\
+            False
 
-        mock_function = mock.Mock(__name__='foo')
-        mock_function.side_effect = exceptions.Forbidden
-        wrapper = decorator(mock_function)
+        @rbac_rv.action(mock.sentinel.service, mock.sentinel.action)
+        def test_policy(*args):
+            raise exceptions.Forbidden()
 
-        mock_authority.PolicyAuthority.return_value.allowed\
-            .return_value = False
-
-        result = wrapper(self.mock_args)
-
-        self.assertIsNone(result)
+        test_policy(self.mock_test_args)
         mock_log.warning.assert_not_called()
         mock_log.error.assert_not_called()
 
@@ -98,89 +93,74 @@
         allowed to perform the action, then the Forbidden exception should be
         raised.
         """
-        decorator = rbac_rv.action(mock.sentinel.service, mock.sentinel.action)
-        mock_function = mock.Mock(__name__='foo')
-        mock_function.side_effect = exceptions.Forbidden
-        wrapper = decorator(mock_function)
+        mock_authority.PolicyAuthority.return_value.allowed.return_value = True
 
-        mock_authority.PolicyAuthority.return_value.allowed\
-            .return_value = True
+        @rbac_rv.action(mock.sentinel.service, mock.sentinel.action)
+        def test_policy(*args):
+            raise exceptions.Forbidden()
 
-        e = self.assertRaises(exceptions.Forbidden, wrapper, self.mock_args)
-        self.assertIn(
-            "Role Member was not allowed to perform sentinel.action.",
-            e.__str__())
+        test_re = "Role Member was not allowed to perform sentinel.action."
+        self.assertRaisesRegex(exceptions.Forbidden, test_re, test_policy,
+                               self.mock_test_args)
         mock_log.error.assert_called_once_with("Role Member was not allowed to"
                                                " perform sentinel.action.")
 
     @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(
-            self, mock_authority_authority, mock_log):
+            self, mock_authority, mock_log):
         """Test RbacMalformedResponse 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.
         """
-        decorator = rbac_rv.action(mock.sentinel.service, mock.sentinel.action)
-        mock_function = mock.Mock(__name__='foo')
-        mock_function.side_effect = rbac_exceptions.RbacMalformedResponse
-        wrapper = decorator(mock_function)
+        mock_authority.PolicyAuthority.return_value.allowed.return_value =\
+            False
 
-        (mock_authority_authority.PolicyAuthority.return_value.allowed
-            .return_value) = False
+        @rbac_rv.action(mock.sentinel.service, mock.sentinel.action)
+        def test_policy(*args):
+            raise rbac_exceptions.RbacMalformedResponse()
 
-        result = wrapper(self.mock_args)
-
-        self.assertIsNone(result)
         mock_log.error.assert_not_called()
         mock_log.warning.assert_not_called()
 
     @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_authority, mock_log):
+            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.
         """
-        decorator = rbac_rv.action(mock.sentinel.service, mock.sentinel.action)
-        mock_function = mock.Mock(__name__='foo')
-        mock_function.side_effect = rbac_exceptions.RbacMalformedResponse
-        wrapper = decorator(mock_function)
+        mock_authority.PolicyAuthority.return_value.allowed.return_value = True
 
-        (mock_authority_authority.PolicyAuthority.return_value.allowed
-            .return_value) = True
+        @rbac_rv.action(mock.sentinel.service, mock.sentinel.action)
+        def test_policy(*args):
+            raise rbac_exceptions.RbacMalformedResponse()
 
-        e = self.assertRaises(exceptions.Forbidden, wrapper, self.mock_args)
-        self.assertIn(
-            "Role Member was not allowed to perform sentinel.action.",
-            e.__str__())
-
+        test_re = "Role Member was not allowed to perform sentinel.action."
+        self.assertRaisesRegex(exceptions.Forbidden, test_re, test_policy,
+                               self.mock_test_args)
         mock_log.error.assert_called_once_with("Role Member was not allowed to"
                                                " perform sentinel.action.")
 
     @mock.patch.object(rbac_rv, 'LOG', autospec=True)
     @mock.patch.object(rbac_rv, 'policy_authority', autospec=True)
     def test_rule_validation_rbac_conflicting_policies_positive(
-            self, mock_authority_authority, mock_log):
+            self, mock_authority, mock_log):
         """Test RbacConflictingPolicies error is thrown without permission passes.
 
         Positive test case: if RbacConflictingPolicies is thrown and the user
         is not allowed to perform the action, then this is a success.
         """
-        decorator = rbac_rv.action(mock.sentinel.service, mock.sentinel.action)
-        mock_function = mock.Mock(__name__='foo')
-        mock_function.side_effect = rbac_exceptions.RbacConflictingPolicies
-        wrapper = decorator(mock_function)
+        mock_authority.PolicyAuthority.return_value.allowed.return_value =\
+            False
 
-        (mock_authority_authority.PolicyAuthority.return_value.allowed
-            .return_value) = False
+        @rbac_rv.action(mock.sentinel.service, mock.sentinel.action)
+        def test_policy(*args):
+            raise rbac_exceptions.RbacConflictingPolicies()
 
-        result = wrapper(self.mock_args)
-
-        self.assertIsNone(result)
         mock_log.error.assert_not_called()
         mock_log.warning.assert_not_called()
 
@@ -194,19 +174,15 @@
         Negative test case: if RbacConflictingPolicies is thrown and the user
         is allowed to perform the action, then this is an expected failure.
         """
-        decorator = rbac_rv.action(mock.sentinel.service, mock.sentinel.action)
-        mock_function = mock.Mock(__name__='foo')
-        mock_function.side_effect = rbac_exceptions.RbacConflictingPolicies
-        wrapper = decorator(mock_function)
+        mock_authority.PolicyAuthority.return_value.allowed.return_value = True
 
-        mock_authority.PolicyAuthority.return_value.allowed\
-            .return_value = True
+        @rbac_rv.action(mock.sentinel.service, mock.sentinel.action)
+        def test_policy(*args):
+            raise rbac_exceptions.RbacConflictingPolicies()
 
-        e = self.assertRaises(exceptions.Forbidden, wrapper, self.mock_args)
-        self.assertIn(
-            "Role Member was not allowed to perform sentinel.action.",
-            e.__str__())
-
+        test_re = "Role Member was not allowed to perform sentinel.action."
+        self.assertRaisesRegex(exceptions.Forbidden, test_re, test_policy,
+                               self.mock_test_args)
         mock_log.error.assert_called_once_with("Role Member was not allowed to"
                                                " perform sentinel.action.")
 
@@ -222,24 +198,22 @@
         2) Test have permission and 404 is expected but 403 is thrown throws
            exception.
         """
-        decorator = rbac_rv.action(mock.sentinel.service,
-                                   mock.sentinel.action,
-                                   expected_error_code=404)
-        mock_function = mock.Mock(__name__='foo')
-        mock_function.side_effect = exceptions.Forbidden('Random message.')
-        wrapper = decorator(mock_function)
+        @rbac_rv.action(mock.sentinel.service, mock.sentinel.action,
+                        expected_error_code=404)
+        def test_policy(*args):
+            raise exceptions.Forbidden('Test message')
 
-        expected_error = "An unexpected exception has occurred during test: "\
-            "foo, Exception was: Forbidden\nDetails: Random message."
+        error_re = ("An unexpected exception has occurred during test: "
+                    "test_policy. Exception was: Forbidden\nDetails: Test "
+                    "message")
 
         for allowed in [True, False]:
             mock_authority.PolicyAuthority.return_value.allowed.\
                 return_value = allowed
 
-            e = self.assertRaises(exceptions.Forbidden, wrapper,
-                                  self.mock_args)
-            self.assertIn(expected_error, e.__str__())
-            mock_log.error.assert_called_once_with(expected_error)
+            self.assertRaisesRegex(exceptions.Forbidden, '.* ' + error_re,
+                                   test_policy, self.mock_test_args)
+            self.assertIn(error_re, mock_log.error.mock_calls[0][1][0])
             mock_log.error.reset_mock()
 
     @mock.patch.object(rbac_rv, 'LOG', autospec=True)
@@ -255,12 +229,10 @@
         In both cases, a LOG.warning is called with the "irregular message"
         that signals to user that a 404 was expected and caught.
         """
-        decorator = rbac_rv.action(mock.sentinel.service,
-                                   mock.sentinel.action,
-                                   expected_error_code=404)
-        mock_function = mock.Mock(__name__='foo')
-        mock_function.side_effect = exceptions.NotFound
-        wrapper = decorator(mock_function)
+        @rbac_rv.action(mock.sentinel.service, mock.sentinel.action,
+                        expected_error_code=404)
+        def test_policy(*args):
+            raise exceptions.NotFound()
 
         expected_errors = [
             "Role Member was not allowed to perform sentinel.action.", None
@@ -273,12 +245,12 @@
             expected_error = expected_errors[pos]
 
             if expected_error:
-                e = self.assertRaises(exceptions.Forbidden, wrapper,
-                                      self.mock_args)
-                self.assertIn(expected_error, e.__str__())
+                self.assertRaisesRegex(
+                    exceptions.Forbidden, '.* ' + expected_error, test_policy,
+                    self.mock_test_args)
                 mock_log.error.assert_called_once_with(expected_error)
             else:
-                wrapper(self.mock_args)
+                test_policy(self.mock_test_args)
                 mock_log.error.assert_not_called()
 
             mock_log.warning.assert_called_once_with(
@@ -299,43 +271,75 @@
         Tests that case where no exception is thrown but the Patrole framework
         says that the role should not be allowed to perform the policy action.
         """
-        decorator = rbac_rv.action(mock.sentinel.service, mock.sentinel.action)
+        mock_authority.PolicyAuthority.return_value.allowed.return_value =\
+            False
 
-        mock_function = mock.Mock(__name__='foo')
-        wrapper = decorator(mock_function)
+        @rbac_rv.action(mock.sentinel.service, mock.sentinel.action)
+        def test_policy_expect_forbidden(*args):
+            pass
 
-        mock_authority.PolicyAuthority.return_value.allowed\
-            .return_value = False
+        @rbac_rv.action(mock.sentinel.service, mock.sentinel.action,
+                        expected_error_code=404)
+        def test_policy_expect_not_found(*args):
+            pass
 
-        e = self.assertRaises(rbac_exceptions.RbacOverPermission, wrapper,
-                              self.mock_args)
-        self.assertIn(("OverPermission: Role Member was allowed to perform "
-                       "sentinel.action"), e.__str__())
-        mock_log.error.assert_called_once_with(
-            'Role %s was allowed to perform %s', 'Member',
-            mock.sentinel.action)
+        for test_policy in (
+            test_policy_expect_forbidden, test_policy_expect_not_found):
+
+            error_re = (".* OverPermission: Role Member was allowed to perform"
+                        " sentinel.action")
+            self.assertRaisesRegex(rbac_exceptions.RbacOverPermission,
+                                   error_re, test_policy, self.mock_test_args)
+            mock_log.error.assert_called_once_with(
+                'Role %s was allowed to perform %s', 'Member',
+                mock.sentinel.action)
+            mock_log.error.reset_mock()
 
     @mock.patch.object(rbac_rv, 'policy_authority', autospec=True)
-    def test_invalid_policy_rule_throws_parsing_exception(
-            self, mock_authority_authority):
-        """Test that invalid policy action causes test to be skipped."""
-        CONF.set_override('strict_policy_check', True, group='patrole')
-        self.addCleanup(CONF.clear_override, 'strict_policy_check',
-                        group='patrole')
+    def test_invalid_policy_rule_raises_parsing_exception(
+            self, mock_authority):
+        """Test that invalid policy action causes test to be fail with
+        ``[patrole] strict_policy_check`` set to True.
+        """
+        self.useFixture(
+            fixtures.ConfPatcher(strict_policy_check=True, group='patrole'))
 
-        mock_authority_authority.PolicyAuthority.return_value.allowed.\
+        mock_authority.PolicyAuthority.return_value.allowed.\
             side_effect = rbac_exceptions.RbacParsingException
 
-        decorator = rbac_rv.action(mock.sentinel.service,
-                                   mock.sentinel.policy_rule)
-        wrapper = decorator(mock.Mock(__name__='foo'))
+        @rbac_rv.action(mock.sentinel.service, mock.sentinel.action)
+        def test_policy(*args):
+            pass
 
-        e = self.assertRaises(rbac_exceptions.RbacParsingException, wrapper,
-                              self.mock_args)
-        self.assertEqual('Attempted to test an invalid policy file or action',
-                         str(e))
+        error_re = 'Attempted to test an invalid policy file or action'
+        self.assertRaisesRegex(rbac_exceptions.RbacParsingException, error_re,
+                               test_policy, self.mock_test_args)
 
-        mock_authority_authority.PolicyAuthority.assert_called_once_with(
+        mock_authority.PolicyAuthority.assert_called_once_with(
+            mock.sentinel.project_id, mock.sentinel.user_id,
+            mock.sentinel.service, extra_target_data={})
+
+    @mock.patch.object(rbac_rv, 'policy_authority', autospec=True)
+    def test_invalid_policy_rule_raises_skip_exception(
+            self, mock_authority):
+        """Test that invalid policy action causes test to be skipped with
+        ``[patrole] strict_policy_check`` set to False.
+        """
+        self.useFixture(
+            fixtures.ConfPatcher(strict_policy_check=False, group='patrole'))
+
+        mock_authority.PolicyAuthority.return_value.allowed.side_effect = (
+            rbac_exceptions.RbacParsingException)
+
+        @rbac_rv.action(mock.sentinel.service, mock.sentinel.action)
+        def test_policy(*args):
+            pass
+
+        error_re = 'Attempted to test an invalid policy file or action'
+        self.assertRaisesRegex(testtools.TestCase.skipException, error_re,
+                               test_policy, self.mock_test_args)
+
+        mock_authority.PolicyAuthority.assert_called_once_with(
             mock.sentinel.project_id, mock.sentinel.user_id,
             mock.sentinel.service, extra_target_data={})
 
@@ -393,47 +397,41 @@
     @mock.patch.object(rbac_rv, 'RBACLOG', autospec=True)
     @mock.patch.object(rbac_rv, 'policy_authority', autospec=True)
     def test_rbac_report_logging_disabled(self, mock_authority, mock_rbaclog):
-        """Test case to ensure that we DON'T write logs when
-        enable_reporting is False
+        """Test case to ensure that we DON'T write logs when  enable_reporting
+        is False
         """
-        CONF.set_override('enable_reporting', False, group='patrole_log')
-        self.addCleanup(CONF.clear_override,
-                        'enable_reporting', group='patrole_log')
-
-        decorator = rbac_rv.action(mock.sentinel.service, mock.sentinel.action)
-
-        mock_function = mock.Mock(__name__='foo-nolog')
-        wrapper = decorator(mock_function)
+        self.useFixture(
+            fixtures.ConfPatcher(enable_reporting=False, group='patrole_log'))
 
         mock_authority.PolicyAuthority.return_value.allowed.return_value = True
 
-        wrapper(self.mock_args)
+        @rbac_rv.action(mock.sentinel.service, mock.sentinel.action)
+        def test_policy(*args):
+            pass
 
+        test_policy(self.mock_test_args)
         self.assertFalse(mock_rbaclog.info.called)
 
     @mock.patch.object(rbac_rv, 'RBACLOG', autospec=True)
     @mock.patch.object(rbac_rv, 'policy_authority', autospec=True)
     def test_rbac_report_logging_enabled(self, mock_authority, mock_rbaclog):
-        """Test case to ensure that we DO write logs when
-        enable_reporting is True
+        """Test case to ensure that we DO write logs when enable_reporting is
+        True
         """
-        CONF.set_override('enable_reporting', True, group='patrole_log')
-        self.addCleanup(CONF.clear_override,
-                        'enable_reporting', group='patrole_log')
-
-        decorator = rbac_rv.action(mock.sentinel.service, mock.sentinel.action)
-
-        mock_function = mock.Mock(__name__='foo-log')
-        wrapper = decorator(mock_function)
+        self.useFixture(
+            fixtures.ConfPatcher(enable_reporting=True, group='patrole_log'))
 
         mock_authority.PolicyAuthority.return_value.allowed.return_value = True
 
-        wrapper(self.mock_args)
+        @rbac_rv.action(mock.sentinel.service, mock.sentinel.action)
+        def test_policy(*args):
+            pass
 
+        test_policy(self.mock_test_args)
         mock_rbaclog.info.assert_called_once_with(
             "[Service]: %s, [Test]: %s, [Rule]: %s, "
             "[Expected]: %s, [Actual]: %s",
-            mock.sentinel.service, 'foo-log',
+            mock.sentinel.service, 'test_policy',
             mock.sentinel.action,
             "Allowed",
             "Allowed")
diff --git a/releasenotes/notes/backup-project-attribute-test-504f053c6ec95b85.yaml b/releasenotes/notes/backup-project-attribute-test-504f053c6ec95b85.yaml
new file mode 100644
index 0000000..01a55cc
--- /dev/null
+++ b/releasenotes/notes/backup-project-attribute-test-504f053c6ec95b85.yaml
@@ -0,0 +1,6 @@
+---
+features:
+  - |
+    Add RBAC test for "backup:backup_project_attribute" which verifies
+    that the "os-backup-project-attr:project_id" attribute appears in
+    the response body once policy enforcement succeeds.
diff --git a/requirements.txt b/requirements.txt
index 00c7e64..abccb62 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -3,9 +3,8 @@
 # process, which may cause wedges in the gate later.
 hacking!=0.13.0,<0.14,>=0.12.0 # Apache-2.0
 pbr!=2.1.0,>=2.0.0 # Apache-2.0
-urllib3>=1.21.1 # MIT
 oslo.log>=3.30.0 # Apache-2.0
-oslo.config!=4.3.0,!=4.4.0,>=4.0.0 # Apache-2.0
+oslo.config>=4.6.0 # Apache-2.0
 oslo.policy>=1.23.0 # Apache-2.0
 tempest>=16.1.0 # Apache-2.0
 stevedore>=1.20.0 # Apache-2.0
diff --git a/test-requirements.txt b/test-requirements.txt
index 0657438..dc2fec9 100644
--- a/test-requirements.txt
+++ b/test-requirements.txt
@@ -4,13 +4,13 @@
 hacking!=0.13.0,<0.14,>=0.12.0 # Apache-2.0
 
 sphinx>=1.6.2 # BSD
-openstackdocstheme>=1.16.0 # Apache-2.0
+openstackdocstheme>=1.17.0 # Apache-2.0
 reno>=2.5.0 # Apache-2.0
 fixtures>=3.0.0 # Apache-2.0/BSD
 mock>=2.0.0 # BSD
 coverage!=4.4,>=4.0 # Apache-2.0
-nose # LGPL
-nosexcover # BSD
+nose>=1.3.7 # LGPL
+nosexcover>=1.0.10 # BSD
 oslotest>=1.10.0 # Apache-2.0
 oslo.policy>=1.23.0 # Apache-2.0
 oslo.log>=3.30.0 # Apache-2.0