Merge "Replace os with os_primary"
diff --git a/patrole_tempest_plugin/config.py b/patrole_tempest_plugin/config.py
index cb00269..1180836 100644
--- a/patrole_tempest_plugin/config.py
+++ b/patrole_tempest_plugin/config.py
@@ -31,6 +31,8 @@
                 help="If true, throws RbacParsingException for"
                      " policies which don't exist. If false, "
                      "throws skipException."),
+    # TODO(rb560u): There needs to be support for reading these JSON files from
+    # other hosts. It may be possible to leverage the v3 identity policy API
     cfg.StrOpt('cinder_policy_file',
                default='/etc/cinder/policy.json',
                help="Location of the neutron policy file."),
@@ -45,5 +47,56 @@
                help="Location of the neutron policy file."),
     cfg.StrOpt('nova_policy_file',
                default='/etc/nova/policy.json',
-               help="Location of the nova policy file.")
+               help="Location of the nova policy file."),
+    cfg.BoolOpt('test_custom_requirements',
+                default=False,
+                help="""
+This option determines whether Patrole should run against a
+`custom_requirements_file` which defines RBAC requirements. The
+purpose of setting this flag to True is to verify that RBAC policy
+is in accordance to requirements. The idea is that the
+`custom_requirements_file` perfectly defines what the RBAC requirements are.
+
+Here are the possible outcomes when running the Patrole tests against
+a `custom_requirements_file`:
+
+YAML definition: allowed
+test run: allowed
+test result: pass
+
+YAML definition: allowed
+test run: not allowed
+test result: fail (under-permission)
+
+YAML definition: not allowed
+test run: allowed
+test result: fail (over-permission)
+"""),
+    cfg.StrOpt('custom_requirements_file',
+               help="""
+File path of the yaml file that defines your RBAC requirements. This
+file must be located on the same host that Patrole runs on. The yaml
+file should be written as follows:
+
+```
+<service>:
+  <api_action>:
+    - <allowed_role>
+    - <allowed_role>
+    - <allowed_role>
+  <api_action>:
+    - <allowed_role>
+    - <allowed_role>
+<service>
+  <api_action>:
+    - <allowed_role>
+```
+Where:
+service = the service that is being tested (cinder, nova, etc)
+api_action = the policy action that is being tested. Examples:
+             - volume:create
+             - os_compute_api:servers:start
+             - add_image
+allowed_role = the Keystone role that is allowed to perform the API
+""")
 ]
diff --git a/patrole_tempest_plugin/rbac_policy_parser.py b/patrole_tempest_plugin/rbac_policy_parser.py
index bb34f6c..17a626c 100644
--- a/patrole_tempest_plugin/rbac_policy_parser.py
+++ b/patrole_tempest_plugin/rbac_policy_parser.py
@@ -25,12 +25,13 @@
 from tempest.common import credentials_factory as credentials
 
 from patrole_tempest_plugin import rbac_exceptions
+from patrole_tempest_plugin.rbac_utils import RbacAuthority
 
 CONF = cfg.CONF
 LOG = logging.getLogger(__name__)
 
 
-class RbacPolicyParser(object):
+class RbacPolicyParser(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
diff --git a/patrole_tempest_plugin/rbac_rule_validation.py b/patrole_tempest_plugin/rbac_rule_validation.py
index 53f84ff..c088ce7 100644
--- a/patrole_tempest_plugin/rbac_rule_validation.py
+++ b/patrole_tempest_plugin/rbac_rule_validation.py
@@ -25,6 +25,7 @@
 
 from patrole_tempest_plugin import rbac_exceptions
 from patrole_tempest_plugin import rbac_policy_parser
+from patrole_tempest_plugin import requirements_authority
 
 CONF = config.CONF
 LOG = logging.getLogger(__name__)
@@ -39,6 +40,9 @@
     A decorator which allows for positive and negative RBAC testing. Given
     an OpenStack service and a policy action enforced by that service, an
     oslo.policy lookup is performed by calling `authority.get_permission`.
+    Alternatively, the RBAC tests can run against a YAML file that defines
+    policy requirements.
+
     The following cases are possible:
 
     * If `allowed` is True and the test passes, this is a success.
@@ -130,6 +134,20 @@
 
 
 def _is_authorized(test_obj, service, rule_name, extra_target_data):
+    """Validates whether current RBAC role has permission to do policy action.
+
+    :param test_obj: type BaseTestCase (tempest base test class)
+    :param service: the OpenStack service that enforces ``rule_name``
+    :param rule_name: the name of the policy action
+    :param extra_target_data: dictionary with unresolved string literals that
+        reference nested BaseTestCase attributes
+    :returns: True if the current RBAC role can perform the policy action else
+        False
+    :raises RbacParsingException: if ``CONF.rbac.strict_policy_check`` is
+        enabled and the ``rule_name`` does not exist in the system
+    :raises skipException: if ``CONF.rbac.strict_policy_check`` is
+        disabled and the ``rule_name`` does not exist in the system
+    """
     try:
         project_id = test_obj.auth_provider.credentials.project_id
         user_id = test_obj.auth_provider.credentials.user_id
@@ -141,12 +159,17 @@
 
     try:
         role = CONF.rbac.rbac_test_role
-        formatted_target_data = _format_extra_target_data(
-            test_obj, extra_target_data)
-        policy_parser = rbac_policy_parser.RbacPolicyParser(
-            project_id, user_id, service,
-            extra_target_data=formatted_target_data)
-        is_allowed = policy_parser.allowed(rule_name, role)
+        # Test RBAC against custom requirements. Otherwise use oslo.policy
+        if CONF.rbac.test_custom_requirements:
+            authority = requirements_authority.RequirementsAuthority(
+                CONF.rbac.custom_requirements_file, service)
+        else:
+            formatted_target_data = _format_extra_target_data(
+                test_obj, extra_target_data)
+            authority = rbac_policy_parser.RbacPolicyParser(
+                project_id, user_id, service,
+                extra_target_data=formatted_target_data)
+        is_allowed = authority.allowed(rule_name, role)
 
         if is_allowed:
             LOG.debug("[Action]: %s, [Role]: %s is allowed!", rule_name,
@@ -215,7 +238,8 @@
     :param test_obj: type BaseTestCase (tempest base test class)
     :param extra_target_data: dictionary with unresolved string literals that
         reference nested BaseTestCase attributes
-    :returns: dictionary with resolved BaseTestCase attributes
+    :returns: dictionary containing additional object data needed by
+        oslo.policy to validate generic checks
     """
     attr_value = test_obj
     formatted_target_data = {}
diff --git a/patrole_tempest_plugin/rbac_utils.py b/patrole_tempest_plugin/rbac_utils.py
index 3bb2cbd..00bfd24 100644
--- a/patrole_tempest_plugin/rbac_utils.py
+++ b/patrole_tempest_plugin/rbac_utils.py
@@ -13,6 +13,8 @@
 #    License for the specific language governing permissions and limitations
 #    under the License.
 
+import abc
+import six
 import sys
 import time
 
@@ -170,3 +172,13 @@
         :returns: True if ``rbac_test_role`` is the admin role.
         """
         return CONF.rbac.rbac_test_role == CONF.identity.admin_role
+
+
+@six.add_metaclass(abc.ABCMeta)
+class RbacAuthority(object):
+    # TODO(rb560u): Add documentation explaining what this class is for
+
+    @abc.abstractmethod
+    def allowed(self, rule_name, role):
+        """Determine whether the role should be able to perform the API"""
+        return
diff --git a/patrole_tempest_plugin/requirements_authority.py b/patrole_tempest_plugin/requirements_authority.py
new file mode 100644
index 0000000..2db12db
--- /dev/null
+++ b/patrole_tempest_plugin/requirements_authority.py
@@ -0,0 +1,72 @@
+# Copyright 2017 AT&T Corporation.
+# All Rights Reserved.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+import yaml
+
+from oslo_log import log as logging
+
+from tempest.lib import exceptions
+
+from patrole_tempest_plugin.rbac_utils import RbacAuthority
+
+LOG = logging.getLogger(__name__)
+
+
+class RequirementsParser(object):
+    _inner = None
+
+    class Inner(object):
+        _rbac_map = None
+
+        def __init__(self, filepath):
+            with open(filepath) as f:
+                RequirementsParser.Inner._rbac_map = \
+                    list(yaml.safe_load_all(f))
+
+    def __init__(self, filepath):
+        if RequirementsParser._inner is None:
+            RequirementsParser._inner = RequirementsParser.Inner(filepath)
+
+    @staticmethod
+    def parse(component):
+        try:
+            for section in RequirementsParser.Inner._rbac_map:
+                if component in section:
+                    return section[component]
+        except yaml.parser.ParserError:
+            LOG.error("Error while parsing the requirements YAML file. Did "
+                      "you pass a valid component name from the test case?")
+        return None
+
+
+class RequirementsAuthority(RbacAuthority):
+    def __init__(self, filepath=None, component=None):
+        if filepath is not None and component is not None:
+            self.roles_dict = RequirementsParser(filepath).parse(component)
+        else:
+            self.roles_dict = None
+
+    def allowed(self, rule_name, role):
+        if self.roles_dict is None:
+            raise exceptions.InvalidConfiguration(
+                "Roles dictionary parsed from requirements YAML file is "
+                "empty. Ensure the requirements YAML file is correctly "
+                "formatted.")
+        try:
+            _api = self.roles_dict[rule_name]
+            return role in _api
+        except KeyError:
+            raise KeyError("'%s' API is not defined in the requirements YAML "
+                           "file" % rule_name)
+        return False
diff --git a/patrole_tempest_plugin/tests/api/volume/rbac_base.py b/patrole_tempest_plugin/tests/api/volume/rbac_base.py
index 1d390b7..953a834 100644
--- a/patrole_tempest_plugin/tests/api/volume/rbac_base.py
+++ b/patrole_tempest_plugin/tests/api/volume/rbac_base.py
@@ -47,6 +47,8 @@
         }
         cls.volume_hosts_client, cls.volume_types_client = \
             version_checker[cls._api_version]
+        cls.groups_client = cls.os_primary.groups_v3_client
+        cls.group_types_client = cls.os_primary.group_types_v3_client
 
     @classmethod
     def resource_setup(cls):
@@ -56,6 +58,8 @@
     @classmethod
     def resource_cleanup(cls):
         super(BaseVolumeRbacTest, cls).resource_cleanup()
+        # Allow volumes to be cleared first, so only clear volume types
+        # after super's resource_cleanup.
         cls.clear_volume_types()
 
     @classmethod
@@ -64,15 +68,33 @@
         name = name or data_utils.rand_name(cls.__name__ + '-volume-type')
         volume_type = cls.volume_types_client.create_volume_type(
             name=name, **kwargs)['volume_type']
-        cls.volume_types.append(volume_type['id'])
+        cls.volume_types.append(volume_type)
         return volume_type
 
+    def create_group_type(self, name=None, ignore_notfound=False, **kwargs):
+        """Create a test group-type"""
+        name = name or data_utils.rand_name(
+            self.__class__.__name__ + '-group-type')
+        group_type = self.group_types_client.create_group_type(
+            name=name, **kwargs)['group_type']
+
+        if ignore_notfound:
+            self.addCleanup(test_utils.call_and_ignore_notfound_exc,
+                            self.group_types_client.delete_group_type,
+                            group_type['id'])
+        else:
+            self.addCleanup(self.group_types_client.delete_group_type,
+                            group_type['id'])
+
+        return group_type
+
     @classmethod
     def clear_volume_types(cls):
         for vol_type in cls.volume_types:
             test_utils.call_and_ignore_notfound_exc(
-                cls.volume_types_client.delete_volume_type, vol_type)
+                cls.volume_types_client.delete_volume_type, vol_type['id'])
 
         for vol_type in cls.volume_types:
             test_utils.call_and_ignore_notfound_exc(
-                cls.volume_types_client.wait_for_resource_deletion, vol_type)
+                cls.volume_types_client.wait_for_resource_deletion,
+                vol_type['id'])
diff --git a/patrole_tempest_plugin/tests/api/volume/test_groups_rbac.py b/patrole_tempest_plugin/tests/api/volume/test_groups_rbac.py
new file mode 100644
index 0000000..6b07aaa
--- /dev/null
+++ b/patrole_tempest_plugin/tests/api/volume/test_groups_rbac.py
@@ -0,0 +1,143 @@
+# Copyright 2017 AT&T Corporation.
+# All Rights Reserved.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+from tempest.common import waiters
+from tempest.lib.common.utils import data_utils
+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
+
+
+class GroupsV3RbacTest(rbac_base.BaseVolumeRbacTest):
+    _api_version = 3
+    min_microversion = '3.14'
+    max_microversion = 'latest'
+
+    def setUp(self):
+        super(GroupsV3RbacTest, self).setUp()
+        self.volume_type_id = self.create_volume_type()['id']
+        self.group_type_id = self.create_group_type()['id']
+
+    def _create_group(self, name=None, ignore_notfound=False, **kwargs):
+        group_name = name or data_utils.rand_name(
+            self.__class__.__name__ + '-Group')
+        group = self.groups_client.create_group(name=group_name, **kwargs)[
+            'group']
+        waiters.wait_for_volume_resource_status(
+            self.groups_client, group['id'], 'available')
+
+        if ignore_notfound:
+            self.addCleanup(test_utils.call_and_ignore_notfound_exc,
+                            self._delete_group, group['id'])
+        else:
+            self.addCleanup(self._delete_group, group['id'])
+
+        return group
+
+    def _delete_group(self, group_id, delete_volumes=True):
+        self.groups_client.delete_group(group_id, delete_volumes)
+        self.groups_client.wait_for_resource_deletion(group_id)
+
+    @decorators.idempotent_id('43235328-66ae-424f-bc7f-f709c0ca268c')
+    @rbac_rule_validation.action(
+        service="cinder",
+        rule="group:create")
+    def test_create_group(self):
+        self.rbac_utils.switch_role(self, toggle_rbac_role=True)
+        self._create_group(ignore_notfound=True,
+                           group_type=self.group_type_id,
+                           volume_types=[self.volume_type_id])
+
+    @decorators.idempotent_id('9dc34a62-ae3e-439e-92b6-9389ea4c2863')
+    @rbac_rule_validation.action(
+        service="cinder",
+        rule="group:get")
+    def test_show_group(self):
+        group = self._create_group(group_type=self.group_type_id,
+                                   volume_types=[self.volume_type_id])
+
+        self.rbac_utils.switch_role(self, toggle_rbac_role=True)
+        self.groups_client.show_group(group['id'])
+
+    @decorators.idempotent_id('db43841b-a173-4317-acfc-f83e4e48e4ee')
+    @rbac_rule_validation.action(
+        service="cinder",
+        rule="group:get_all")
+    def test_list_groups(self):
+        self.rbac_utils.switch_role(self, toggle_rbac_role=True)
+        self.groups_client.list_groups()['groups']
+
+    @decorators.idempotent_id('5378da93-9c26-4ad4-b039-0555e2b8f668')
+    @rbac_rule_validation.action(
+        service="cinder",
+        rule="group:get_all")
+    def test_list_groups_with_details(self):
+        self.rbac_utils.switch_role(self, toggle_rbac_role=True)
+        self.groups_client.list_groups(detail=True)['groups']
+
+    @decorators.idempotent_id('66fda391-5774-42a9-a018-80b34e57ab76')
+    @rbac_rule_validation.action(
+        service="cinder",
+        rule="group:delete")
+    def test_delete_group(self):
+        group = self._create_group(ignore_notfound=True,
+                                   group_type=self.group_type_id,
+                                   volume_types=[self.volume_type_id])
+
+        self.rbac_utils.switch_role(self, toggle_rbac_role=True)
+        self.groups_client.delete_group(group['id'])
+
+
+class GroupTypesV3RbacTest(rbac_base.BaseVolumeRbacTest):
+    _api_version = 3
+    min_microversion = '3.11'
+    max_microversion = 'latest'
+
+    @decorators.idempotent_id('2820f12c-4681-4c7f-b28d-e6925637dff6')
+    @rbac_rule_validation.action(
+        service="cinder",
+        rule="group:group_types_manage")
+    def test_create_group_type(self):
+        self.rbac_utils.switch_role(self, toggle_rbac_role=True)
+        self.create_group_type(ignore_notfound=True)
+
+    @decorators.idempotent_id('a5f88c26-df7c-4f21-a3ae-7a4c2d6212b4')
+    @rbac_rule_validation.action(
+        service="cinder",
+        rule="group:access_group_types_specs")
+    def test_create_group_type_group_specs(self):
+        # TODO(felipemonteiro): Combine with ``test_create_group_type``
+        # once multiple policy testing is supported. This policy is
+        # only enforced after "group:group_types_manage".
+        self.rbac_utils.switch_role(self, toggle_rbac_role=True)
+        group_type = self.create_group_type(ignore_notfound=True)
+
+        if 'group_specs' not in group_type:
+            raise rbac_exceptions.RbacActionFailed(
+                'Policy %s does not return %s in response body.' %
+                ('group:access_group_types_specs', 'group_specs'))
+
+    @decorators.idempotent_id('f77f8156-4fc9-4f02-be15-8930f748e10c')
+    @rbac_rule_validation.action(
+        service="cinder",
+        rule="group:group_types_manage")
+    def test_delete_group_type(self):
+        goup_type = self.create_group_type(ignore_notfound=True)
+
+        self.rbac_utils.switch_role(self, toggle_rbac_role=True)
+        self.group_types_client.delete_group_type(goup_type['id'])
diff --git a/patrole_tempest_plugin/tests/api/volume/test_volume_actions_rbac.py b/patrole_tempest_plugin/tests/api/volume/test_volume_actions_rbac.py
index 8bb92f4..b666a2d 100644
--- a/patrole_tempest_plugin/tests/api/volume/test_volume_actions_rbac.py
+++ b/patrole_tempest_plugin/tests/api/volume/test_volume_actions_rbac.py
@@ -155,10 +155,13 @@
     @rbac_rule_validation.action(service="cinder",
                                  rule="volume:retype")
     def test_volume_retype(self):
-        volume = self.create_volume()
         vol_type = self.create_volume_type()['name']
+        volume = self.create_volume()
+
         self.rbac_utils.switch_role(self, toggle_rbac_role=True)
         self.volumes_client.retype_volume(volume['id'], new_type=vol_type)
+        waiters.wait_for_volume_retype(
+            self.os_admin.volumes_client, volume['id'], vol_type)
 
     @rbac_rule_validation.action(
         service="cinder",
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 dfe6495..e6944cc 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
@@ -57,7 +57,7 @@
     @rbac_rule_validation.action(service="cinder",
                                  rule="backup:create")
     @decorators.idempotent_id('6887ec94-0bcf-4ab7-b30f-3808a4b5a2a5')
-    def test_volume_backup_create(self):
+    def test_create_backup(self):
         self.rbac_utils.switch_role(self, toggle_rbac_role=True)
         self.create_backup(volume_id=self.volume['id'])
 
@@ -65,7 +65,7 @@
     @rbac_rule_validation.action(service="cinder",
                                  rule="backup:get")
     @decorators.idempotent_id('abd92bdd-b0fb-4dc4-9cfc-de9e968f8c8a')
-    def test_volume_backup_get(self):
+    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'])
@@ -73,15 +73,36 @@
     @rbac_rule_validation.action(service="cinder",
                                  rule="backup:get_all")
     @decorators.idempotent_id('4d18f0f0-7e01-4007-b622-dedc859b22f6')
-    def test_volume_backup_list(self):
+    def test_list_backups(self):
         self.rbac_utils.switch_role(self, toggle_rbac_role=True)
         self.backups_client.list_backups()
 
+    @decorators.idempotent_id('dbd69865-876f-4835-b70e-7341153fb162')
+    @rbac_rule_validation.action(service="cinder",
+                                 rule="backup:get_all")
+    def test_list_backups_with_details(self):
+        self.rbac_utils.switch_role(self, toggle_rbac_role=True)
+        self.backups_client.list_backups(detail=True)
+
+    @decorators.idempotent_id('50f43bde-205e-438e-9a05-5eac07fc3d63')
+    @rbac_rule_validation.action(
+        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'])
+
+        self.rbac_utils.switch_role(self, toggle_rbac_role=True)
+        self.backups_client.reset_backup_status(backup_id=backup['id'],
+                                                status='error')
+        waiters.wait_for_volume_resource_status(self.os_admin.backups_client,
+                                                backup['id'], 'error')
+
     @test.attr(type=["slow"])
     @rbac_rule_validation.action(service="cinder",
                                  rule="backup:restore")
     @decorators.idempotent_id('9c794bf9-2446-4f41-8fe0-80b71e757f9d')
-    def test_volume_backup_restore(self):
+    def test_restore_backup(self):
         backup = self.create_backup(volume_id=self.volume['id'])
         self.rbac_utils.switch_role(self, toggle_rbac_role=True)
         restore = self.backups_client.restore_backup(backup['id'])['restore']
@@ -92,7 +113,7 @@
     @rbac_rule_validation.action(service="cinder",
                                  rule="backup:delete")
     @decorators.idempotent_id('d5d0c6a2-413d-437e-a73f-4bf2b41a20ed')
-    def test_volume_backup_delete(self):
+    def test_delete_backup(self):
         # Do not call the create_backup in Tempest's base volume class, because
         # it doesn't use ``test_utils.call_and_ignore_notfound_exc`` for clean
         # up.
@@ -112,7 +133,7 @@
     @rbac_rule_validation.action(service="cinder",
                                  rule="backup:backup-export")
     @decorators.idempotent_id('e984ec8d-e8eb-485c-98bc-f1856020303c')
-    def test_volume_backup_export(self):
+    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']
@@ -121,7 +142,7 @@
     @rbac_rule_validation.action(service="cinder",
                                  rule="backup:backup-import")
     @decorators.idempotent_id('1e70f039-4556-44cc-9cc1-edf2b7ed648b')
-    def test_volume_backup_import(self):
+    def test_import_backup(self):
         backup = self.create_backup(volume_id=self.volume['id'])
         export_backup = self.backups_client.export_backup(
             backup['id'])['backup-record']
diff --git a/patrole_tempest_plugin/tests/unit/resources/rbac_roles.yaml b/patrole_tempest_plugin/tests/unit/resources/rbac_roles.yaml
new file mode 100644
index 0000000..c5436d0
--- /dev/null
+++ b/patrole_tempest_plugin/tests/unit/resources/rbac_roles.yaml
@@ -0,0 +1,6 @@
+Test:
+  test:create:
+    - test_member
+    - _member_
+  test:create2:
+    - test_member
diff --git a/patrole_tempest_plugin/tests/unit/test_requirements_authority.py b/patrole_tempest_plugin/tests/unit/test_requirements_authority.py
new file mode 100644
index 0000000..1fb9636
--- /dev/null
+++ b/patrole_tempest_plugin/tests/unit/test_requirements_authority.py
@@ -0,0 +1,85 @@
+# Copyright 2017 AT&T Corporation.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+import os
+
+from tempest.lib import exceptions
+from tempest.tests import base
+
+from patrole_tempest_plugin import requirements_authority as req_auth
+
+
+class RequirementsAuthorityTest(base.TestCase):
+    def setUp(self):
+        super(RequirementsAuthorityTest, self).setUp()
+        self.rbac_auth = req_auth.RequirementsAuthority()
+        self.current_directory = os.path.dirname(os.path.realpath(__file__))
+        self.yaml_test_file = os.path.join(self.current_directory,
+                                           'resources',
+                                           'rbac_roles.yaml')
+        self.expected_result = {'test:create': ['test_member', '_member_'],
+                                'test:create2': ['test_member']}
+
+    def test_requirements_auth_init(self):
+        rbac_auth = req_auth.RequirementsAuthority(self.yaml_test_file, 'Test')
+        self.assertEqual(self.expected_result, rbac_auth.roles_dict)
+
+    def test_auth_allowed_empty_roles(self):
+        self.rbac_auth.roles_dict = None
+        self.assertRaises(exceptions.InvalidConfiguration,
+                          self.rbac_auth.allowed, "", "")
+
+    def test_auth_allowed_role_in_api(self):
+        self.rbac_auth.roles_dict = {'api': ['_member_']}
+        self.assertTrue(self.rbac_auth.allowed("api", "_member_"))
+
+    def test_auth_allowed_role_not_in_api(self):
+        self.rbac_auth.roles_dict = {'api': ['_member_']}
+        self.assertFalse(self.rbac_auth.allowed("api", "support_member"))
+
+    def test_parser_get_allowed_except_keyerror(self):
+        self.rbac_auth.roles_dict = {}
+        self.assertRaises(KeyError, self.rbac_auth.allowed,
+                          "api", "support_member")
+
+    def test_parser_init(self):
+        req_auth.RequirementsParser(self.yaml_test_file)
+        self.assertEqual([{'Test': self.expected_result}],
+                         req_auth.RequirementsParser.Inner._rbac_map)
+
+    def test_parser_role_in_api(self):
+        req_auth.RequirementsParser.Inner._rbac_map = \
+            [{'Test': self.expected_result}]
+        self.rbac_auth.roles_dict = req_auth.RequirementsParser.parse("Test")
+
+        self.assertEqual(self.expected_result, self.rbac_auth.roles_dict)
+        self.assertTrue(self.rbac_auth.allowed("test:create2", "test_member"))
+
+    def test_parser_role_not_in_api(self):
+        req_auth.RequirementsParser.Inner._rbac_map = \
+            [{'Test': self.expected_result}]
+        self.rbac_auth.roles_dict = req_auth.RequirementsParser.parse("Test")
+
+        self.assertEqual(self.expected_result, self.rbac_auth.roles_dict)
+        self.assertFalse(self.rbac_auth.allowed("test:create2", "_member_"))
+
+    def test_parser_except_invalid_configuration(self):
+        req_auth.RequirementsParser.Inner._rbac_map = \
+            [{'Test': self.expected_result}]
+        self.rbac_auth.roles_dict = \
+            req_auth.RequirementsParser.parse("Failure")
+
+        self.assertIsNone(self.rbac_auth.roles_dict)
+        self.assertRaises(exceptions.InvalidConfiguration,
+                          self.rbac_auth.allowed, "", "")
diff --git a/releasenotes/notes/more-volume-backup-tests-c3f10aa245df2a4b.yaml b/releasenotes/notes/more-volume-backup-tests-c3f10aa245df2a4b.yaml
new file mode 100644
index 0000000..8d71130
--- /dev/null
+++ b/releasenotes/notes/more-volume-backup-tests-c3f10aa245df2a4b.yaml
@@ -0,0 +1,5 @@
+---
+features:
+  - |
+    Add additional RBAC tests to ``VolumesBackupsRbacTest``, providing coverage
+    for "volume_extension:backup_admin_actions:reset_status".
diff --git a/releasenotes/notes/support_requirements_yaml-a90e0188a19421ba.yaml b/releasenotes/notes/support_requirements_yaml-a90e0188a19421ba.yaml
new file mode 100644
index 0000000..d2f5519
--- /dev/null
+++ b/releasenotes/notes/support_requirements_yaml-a90e0188a19421ba.yaml
@@ -0,0 +1,12 @@
+---
+features:
+  - |
+    Add support of running Patrole against a custom requirements YAML that
+    defines RBAC requirements. The YAML file lists all the APIs and the roles
+    that should have access to the APIs. The purpose of running Patrole against
+    a requirements YAML is to verify that the RBAC policy is in accordance to
+    deployment specific requirements. Running Patrole against a requirements
+    YAML is completely optional and can be enabled by setting the
+    ``[rbac] test_custom_requirements`` option to True in Tempest's
+    configuration file. The requirements YAML must be located on the same host
+    that Patrole runs on.
diff --git a/releasenotes/notes/volume-v3-groups-rbac-tests-60bddf6fa509545d.yaml b/releasenotes/notes/volume-v3-groups-rbac-tests-60bddf6fa509545d.yaml
new file mode 100644
index 0000000..92b1123
--- /dev/null
+++ b/releasenotes/notes/volume-v3-groups-rbac-tests-60bddf6fa509545d.yaml
@@ -0,0 +1,12 @@
+---
+features:
+  - |
+    Add RBAC tests for the volume v3 groups and group types APIs, providing
+    coverage for the following policy actions:
+
+    * group:create
+    * group:get
+    * group:get_all
+    * group:delete
+    * group:group_types_manage
+    * group:access_group_types_specs
diff --git a/requirements.txt b/requirements.txt
index 6871057..126a3dc 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -2,10 +2,10 @@
 # of appearance. Changing the order has an impact on the overall integration
 # process, which may cause wedges in the gate later.
 hacking!=0.13.0,<0.14,>=0.12.0 # Apache-2.0
-pbr>=1.8 # Apache-2.0
-urllib3>=1.15.1 # MIT
-oslo.log>=3.11.0 # Apache-2.0
-oslo.config>=3.22.0  # Apache-2.0
-oslo.policy>=1.17.0  # Apache-2.0
-tempest>=14.0.0  # Apache-2.0
-stevedore>=1.20.0  # Apache-2.0
+pbr!=2.1.0,>=2.0.0 # Apache-2.0
+urllib3>=1.21.1 # MIT
+oslo.log>=3.22.0 # Apache-2.0
+oslo.config!=4.3.0,!=4.4.0,>=4.0.0 # Apache-2.0
+oslo.policy>=1.23.0 # Apache-2.0
+tempest>=14.0.0 # Apache-2.0
+stevedore>=1.20.0 # Apache-2.0
diff --git a/setup.py b/setup.py
index f730546..566d844 100644
--- a/setup.py
+++ b/setup.py
@@ -1,5 +1,4 @@
-# Copyright 2017 ATT Corporation.
-# All Rights Reserved.
+# Copyright (c) 2013 Hewlett-Packard Development Company, L.P.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -26,5 +25,5 @@
     pass
 
 setuptools.setup(
-    setup_requires=['pbr>=1.8'],
+    setup_requires=['pbr>=2.0.0'],
     pbr=True)
diff --git a/test-requirements.txt b/test-requirements.txt
index 0639513..3e03437 100644
--- a/test-requirements.txt
+++ b/test-requirements.txt
@@ -1,16 +1,16 @@
 # The order of packages is significant, because pip processes them in the order
 # of appearance. Changing the order has an impact on the overall integration
 # process, which may cause wedges in the gate later.
-hacking>=0.12.0,!=0.13.0,<0.14  # Apache-2.0
+hacking!=0.13.0,<0.14,>=0.12.0 # Apache-2.0
 
-sphinx>=1.2.1,!=1.3b1,<1.4  # BSD
+sphinx>=1.6.2 # BSD
 openstackdocstheme>=1.11.0 # Apache-2.0
-reno>=1.8.0 # Apache-2.0
+reno!=2.3.1,>=1.8.0 # Apache-2.0
 mock>=2.0 # BSD
-coverage>=4.0 # Apache-2.0
+coverage!=4.4,>=4.0 # Apache-2.0
 nose # LGPL
 nosexcover # BSD
 oslotest>=1.10.0 # Apache-2.0
-oslo.policy>=1.17.0  # Apache-2.0
-oslo.log>=3.11.0 # Apache-2.0
-tempest>=12.1.0  # Apache-2.0
+oslo.policy>=1.23.0 # Apache-2.0
+oslo.log>=3.22.0 # Apache-2.0
+tempest>=14.0.0 # Apache-2.0