Merge "Use oslo_policy.policy.Rules.load to load rules"
diff --git a/.zuul.yaml b/.zuul.yaml
index 66339eb..fb110f0 100644
--- a/.zuul.yaml
+++ b/.zuul.yaml
@@ -87,6 +87,11 @@
         RBAC_TEST_ROLE: member
 
 - job:
+    name: patrole-member-rocky
+    parent: patrole-member
+    override-checkout: stable/rocky
+
+- job:
     name: patrole-member-queens
     parent: patrole-member
     override-checkout: stable/queens
@@ -151,6 +156,7 @@
         tempest: true
         neutron: true
         neutron-segments: true
+        neutron-qos: true
 
 - job:
     name: patrole-plugin-member
@@ -184,6 +190,7 @@
       jobs:
         - patrole-admin
         - patrole-member
+        - patrole-member-rocky
         - patrole-member-queens
         - patrole-member-pike
         - patrole-py35-member
@@ -195,3 +202,8 @@
       jobs:
         - patrole-admin
         - patrole-member
+    periodic-stable:
+      jobs:
+        - patrole-member-rocky
+        - patrole-member-queens
+        - patrole-member-pike
diff --git a/devstack/plugin.sh b/devstack/plugin.sh
index 4826d21..a6259f4 100644
--- a/devstack/plugin.sh
+++ b/devstack/plugin.sh
@@ -29,6 +29,9 @@
 
         # These policies were removed in Stein but are available in Pike.
         iniset $TEMPEST_CONFIG policy-feature-enabled removed_nova_policies_stein False
+
+        # TODO(cl566n): Policies used by Patrole testing. Remove these once stable/pike becomes EOL.
+        iniset $TEMPEST_CONFIG policy-feature-enabled added_cinder_policies_stein False
     fi
 
     if [[ ${DEVSTACK_SERIES} == 'queens' ]]; then
@@ -38,9 +41,16 @@
 
         # These policies were removed in Stein but are available in Queens.
         iniset $TEMPEST_CONFIG policy-feature-enabled removed_nova_policies_stein False
+
+        # TODO(cl566n): Policies used by Patrole testing. Remove these once stable/queens becomes EOL.
+        iniset $TEMPEST_CONFIG policy-feature-enabled added_cinder_policies_stein False
     fi
 
-    iniset $TEMPEST_CONFIG patrole enable_rbac True
+    if [[ ${DEVSTACK_SERIES} == 'rocky' ]]; then
+        # TODO(cl566n): Policies used by Patrole testing. Remove these once stable/rocky becomes EOL.
+        iniset $TEMPEST_CONFIG policy-feature-enabled added_cinder_policies_stein False
+    fi
+
     iniset $TEMPEST_CONFIG patrole rbac_test_role $RBAC_TEST_ROLE
 }
 
diff --git a/doc/source/framework/policy_authority.rst b/doc/source/framework/policy_authority.rst
index 822c7b6..37b698c 100644
--- a/doc/source/framework/policy_authority.rst
+++ b/doc/source/framework/policy_authority.rst
@@ -60,3 +60,4 @@
 .. automodule:: patrole_tempest_plugin.policy_authority
    :members:
    :undoc-members:
+   :special-members:
diff --git a/doc/source/framework/rbac_authority.rst b/doc/source/framework/rbac_authority.rst
index 84c372b..40f2a8d 100644
--- a/doc/source/framework/rbac_authority.rst
+++ b/doc/source/framework/rbac_authority.rst
@@ -35,3 +35,4 @@
 .. automodule:: patrole_tempest_plugin.rbac_authority
    :members:
    :undoc-members:
+   :special-members:
diff --git a/doc/source/framework/rbac_utils.rst b/doc/source/framework/rbac_utils.rst
index 7143928..d0fe27e 100644
--- a/doc/source/framework/rbac_utils.rst
+++ b/doc/source/framework/rbac_utils.rst
@@ -176,3 +176,4 @@
 .. automodule:: patrole_tempest_plugin.rbac_utils
    :members:
    :private-members:
+   :special-members:
diff --git a/doc/source/framework/rbac_validation.rst b/doc/source/framework/rbac_validation.rst
index 186dfe2..6cd1534 100644
--- a/doc/source/framework/rbac_validation.rst
+++ b/doc/source/framework/rbac_validation.rst
@@ -17,3 +17,4 @@
 .. automodule:: patrole_tempest_plugin.rbac_rule_validation
    :members:
    :private-members:
+   :special-members:
diff --git a/doc/source/framework/requirements_authority.rst b/doc/source/framework/requirements_authority.rst
index 6c4fcc0..628f0c0 100644
--- a/doc/source/framework/requirements_authority.rst
+++ b/doc/source/framework/requirements_authority.rst
@@ -103,3 +103,4 @@
 .. automodule:: patrole_tempest_plugin.requirements_authority
    :members:
    :undoc-members:
+   :special-members:
diff --git a/patrole_tempest_plugin/config.py b/patrole_tempest_plugin/config.py
index df2a899..56a786b 100644
--- a/patrole_tempest_plugin/config.py
+++ b/patrole_tempest_plugin/config.py
@@ -24,16 +24,6 @@
                default='admin',
                help="""The current RBAC role against which to run
 Patrole tests."""),
-    cfg.BoolOpt('enable_rbac',
-                default=True,
-                deprecated_for_removal=True,
-                deprecated_reason="""This is a legacy option that was
-meaningful when Patrole existed downstream as a suite of tests inside Tempest.
-Installing the Patrole plugin necessarily means that RBAC tests should be run.
-This option is paradoxical with the Tempest plugin architecture.
-""",
-                deprecated_since='R',
-                help="Enables Patrole RBAC tests."),
     cfg.ListOpt('custom_policy_files',
                 default=['/etc/%s/policy.json'],
                 help="""List of the paths to search for policy files. Each
@@ -172,6 +162,11 @@
                 help="""Are the Nova API extension policies available in the
 cloud (e.g. os_compute_api:os-extended-availability-zone)? These policies were
 removed in Stein because Nova API extension concept was removed in Pike."""),
+    cfg.BoolOpt('added_cinder_policies_stein',
+                default=True,
+                help="""Are the Cinder API extension policies available in the
+cloud (e.g. [create|update|get|delete]_encryption_policy)? These policies are
+added in Stein.""")
 ]
 
 
diff --git a/patrole_tempest_plugin/rbac_rule_validation.py b/patrole_tempest_plugin/rbac_rule_validation.py
index a7927fc..d3b057c 100644
--- a/patrole_tempest_plugin/rbac_rule_validation.py
+++ b/patrole_tempest_plugin/rbac_rule_validation.py
@@ -38,8 +38,11 @@
 RBACLOG = logging.getLogger('rbac_reporting')
 
 
-def action(service, rule='', rules=None,
-           expected_error_code=_DEFAULT_ERROR_CODE, expected_error_codes=None,
+def action(service,
+           rule='',
+           rules=None,
+           expected_error_code=_DEFAULT_ERROR_CODE,
+           expected_error_codes=None,
            extra_target_data=None):
     """A decorator for verifying OpenStack policy enforcement.
 
@@ -72,16 +75,18 @@
     As such, negative and positive testing can be applied using this decorator.
 
     :param str service: An OpenStack service. Examples: "nova" or "neutron".
-    :param str rule: (DEPRECATED) A policy action defined in a policy.json file
-        or in code.
-    :param list rules: A list of policy actions defined in a policy.json file
+    :param rule: (DEPRECATED) A policy action defined in a policy.json file
+        or in code. Also accepts a callable that returns a policy action.
+    :type rule: str or callable
+    :param rules: A list of policy actions defined in a policy.json file
         or in code. The rules are logical-ANDed together to derive the expected
-        result.
+        result. Also accepts list of callables that return a policy action.
 
         .. note::
 
             Patrole currently only supports custom JSON policy files.
 
+    :type rules: list[str] or list[callable]
     :param int expected_error_code: (DEPRECATED) 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
@@ -316,7 +321,11 @@
         for i in range(num_rules - num_ecs):
             exp_error_codes.append(_DEFAULT_ERROR_CODE)
 
-    return rules, exp_error_codes
+    evaluated_rules = [
+        r() if callable(r) else r for r in rules
+    ]
+
+    return evaluated_rules, exp_error_codes
 
 
 def _is_authorized(test_obj, service, rule, extra_target_data):
diff --git a/patrole_tempest_plugin/rbac_utils.py b/patrole_tempest_plugin/rbac_utils.py
index c928f40..366e033 100644
--- a/patrole_tempest_plugin/rbac_utils.py
+++ b/patrole_tempest_plugin/rbac_utils.py
@@ -18,7 +18,6 @@
 import time
 
 from oslo_log import log as logging
-from oslo_log import versionutils
 from oslo_utils import excutils
 
 from tempest import clients
@@ -244,18 +243,6 @@
         return [cls.os_primary.auth_provider]
 
     @classmethod
-    def skip_rbac_checks(cls):
-        if not CONF.patrole.enable_rbac:
-            deprecation_msg = ("The `[patrole].enable_rbac` option is "
-                               "deprecated and will be removed in the S "
-                               "release. Patrole tests will always be enabled "
-                               "following installation of the Patrole Tempest "
-                               "plugin. Use a regex to skip tests")
-            versionutils.report_deprecated_feature(LOG, deprecation_msg)
-            raise cls.skipException(
-                'Patrole testing not enabled so skipping %s.' % cls.__name__)
-
-    @classmethod
     def setup_rbac_utils(cls):
         cls.rbac_utils = RbacUtils(cls)
 
diff --git a/patrole_tempest_plugin/tests/api/README.rst b/patrole_tempest_plugin/tests/api/README.rst
new file mode 120000
index 0000000..e2853ec
--- /dev/null
+++ b/patrole_tempest_plugin/tests/api/README.rst
@@ -0,0 +1 @@
+../../../doc/source/field_guide/rbac.rst
\ No newline at end of file
diff --git a/patrole_tempest_plugin/tests/api/compute/rbac_base.py b/patrole_tempest_plugin/tests/api/compute/rbac_base.py
index 99137cc..ab4551e 100644
--- a/patrole_tempest_plugin/tests/api/compute/rbac_base.py
+++ b/patrole_tempest_plugin/tests/api/compute/rbac_base.py
@@ -22,11 +22,6 @@
                             compute_base.BaseV2ComputeTest):
 
     @classmethod
-    def skip_checks(cls):
-        super(BaseV2ComputeRbacTest, cls).skip_checks()
-        cls.skip_rbac_checks()
-
-    @classmethod
     def setup_clients(cls):
         super(BaseV2ComputeRbacTest, cls).setup_clients()
         cls.setup_rbac_utils()
diff --git a/patrole_tempest_plugin/tests/api/identity/rbac_base.py b/patrole_tempest_plugin/tests/api/identity/rbac_base.py
index d34caaa..44f5962 100644
--- a/patrole_tempest_plugin/tests/api/identity/rbac_base.py
+++ b/patrole_tempest_plugin/tests/api/identity/rbac_base.py
@@ -28,11 +28,6 @@
                            base.BaseIdentityTest):
 
     @classmethod
-    def skip_checks(cls):
-        super(BaseIdentityRbacTest, cls).skip_checks()
-        cls.skip_rbac_checks()
-
-    @classmethod
     def setup_clients(cls):
         super(BaseIdentityRbacTest, cls).setup_clients()
         cls.setup_rbac_utils()
diff --git a/patrole_tempest_plugin/tests/api/image/rbac_base.py b/patrole_tempest_plugin/tests/api/image/rbac_base.py
index 2220335..becd564 100644
--- a/patrole_tempest_plugin/tests/api/image/rbac_base.py
+++ b/patrole_tempest_plugin/tests/api/image/rbac_base.py
@@ -20,11 +20,6 @@
                           image_base.BaseV2ImageTest):
 
     @classmethod
-    def skip_checks(cls):
-        super(BaseV2ImageRbacTest, cls).skip_checks()
-        cls.skip_rbac_checks()
-
-    @classmethod
     def setup_clients(cls):
         super(BaseV2ImageRbacTest, cls).setup_clients()
         cls.setup_rbac_utils()
diff --git a/patrole_tempest_plugin/tests/api/network/rbac_base.py b/patrole_tempest_plugin/tests/api/network/rbac_base.py
index 39ad311..6102347 100644
--- a/patrole_tempest_plugin/tests/api/network/rbac_base.py
+++ b/patrole_tempest_plugin/tests/api/network/rbac_base.py
@@ -22,11 +22,6 @@
                           network_base.BaseNetworkTest):
 
     @classmethod
-    def skip_checks(cls):
-        super(BaseNetworkRbacTest, cls).skip_checks()
-        cls.skip_rbac_checks()
-
-    @classmethod
     def setup_clients(cls):
         super(BaseNetworkRbacTest, cls).setup_clients()
         cls.setup_rbac_utils()
diff --git a/patrole_tempest_plugin/tests/api/network/test_address_scope_rbac.py b/patrole_tempest_plugin/tests/api/network/test_address_scope_rbac.py
new file mode 100644
index 0000000..cf73669
--- /dev/null
+++ b/patrole_tempest_plugin/tests/api/network/test_address_scope_rbac.py
@@ -0,0 +1,139 @@
+# Copyright 2018 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 utils
+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_rule_validation
+from patrole_tempest_plugin.tests.api.network import rbac_base as base
+
+
+class AddressScopeRbacTest(base.BaseNetworkPluginRbacTest):
+
+    @classmethod
+    def skip_checks(cls):
+        super(AddressScopeRbacTest, cls).skip_checks()
+        if not utils.is_extension_enabled('address-scope', 'network'):
+            msg = "address-scope extension not enabled."
+            raise cls.skipException(msg)
+
+    @classmethod
+    def resource_setup(cls):
+        super(AddressScopeRbacTest, cls).resource_setup()
+        cls.network = cls.create_network()
+
+    def _create_address_scope(self, name=None, **kwargs):
+        name = name or data_utils.rand_name(self.__class__.__name__)
+        address_scope = self.ntp_client.create_address_scope(name=name,
+                                                             ip_version=6,
+                                                             **kwargs)
+        address_scope = address_scope['address_scope']
+        self.addCleanup(test_utils.call_and_ignore_notfound_exc,
+                        self.ntp_client.delete_address_scope,
+                        address_scope['id'])
+        return address_scope
+
+    @rbac_rule_validation.action(service="neutron",
+                                 rules=["create_address_scope"],
+                                 expected_error_codes=[403])
+    @decorators.idempotent_id('8cb2d6b5-23c2-4648-997b-7a6ae55be3ad')
+    def test_create_address_scope(self):
+
+        """Create Address Scope
+
+        RBAC test for the neutron create_address_scope policy
+        """
+        with self.rbac_utils.override_role(self):
+            self._create_address_scope()
+
+    @rbac_rule_validation.action(service="neutron",
+                                 rules=["create_address_scope",
+                                        "create_address_scope:shared"],
+                                 expected_error_codes=[403, 403])
+    @decorators.idempotent_id('0c3f55c0-6ebe-4251-afca-62c5cb4632ca')
+    def test_create_address_scope_shared(self):
+
+        """Create Shared Address Scope
+
+        RBAC test for the neutron create_address_scope:shared policy
+        """
+        with self.rbac_utils.override_role(self):
+            self._create_address_scope(shared=True)
+
+    @rbac_rule_validation.action(service="neutron",
+                                 rules=["get_address_scope"],
+                                 expected_error_codes=[404])
+    @decorators.idempotent_id('a53f741b-46f6-412f-936f-ac920d449da8')
+    def test_get_address_scope(self):
+
+        """Get Address Scope
+
+        RBAC test for the neutron get_address_scope policy
+        """
+        address_scope = self._create_address_scope()
+        with self.rbac_utils.override_role(self):
+            self.ntp_client.show_address_scope(address_scope['id'])
+
+    @rbac_rule_validation.action(service="neutron",
+                                 rules=["get_address_scope",
+                                        "update_address_scope"],
+                                 expected_error_codes=[404, 403])
+    @decorators.idempotent_id('3ce4d606-e067-4ef5-840f-96c680226e73')
+    def test_update_address_scope(self):
+
+        """Update Address Scope
+
+        RBAC test for neutron update_address_scope policy
+        """
+        address_scope = self._create_address_scope()
+        name = data_utils.rand_name(self.__class__.__name__)
+        with self.rbac_utils.override_role(self):
+            self.ntp_client.update_address_scope(address_scope['id'],
+                                                 name=name)
+
+    @rbac_rule_validation.action(service="neutron",
+                                 rules=["get_address_scope",
+                                        "update_address_scope",
+                                        "update_address_scope:shared"],
+                                 expected_error_codes=[404, 403, 403])
+    @decorators.idempotent_id('77d3a9d2-721a-4d9f-9654-6b52f113df85')
+    def test_update_address_scope_shared(self):
+
+        """Update Shared Address Scope
+
+        RBAC test for neutron update_address_scope:shared policy
+        """
+        address_scope = self._create_address_scope(shared=True)
+        with self.rbac_utils.override_role(self):
+            self.ntp_client.update_address_scope(address_scope['id'],
+                                                 shared=False)
+
+    @rbac_rule_validation.action(service="neutron",
+                                 rules=["get_address_scope",
+                                        "delete_address_scope"],
+                                 expected_error_codes=[404, 403])
+    @decorators.idempotent_id('277d8e47-e498-4452-b969-a91f747296ba')
+    def test_delete_address_scope(self):
+
+        """Delete Address Scope
+
+        RBAC test for neutron delete_address_scope policy
+        """
+        address_scope = self._create_address_scope()
+        with self.rbac_utils.override_role(self):
+            self.ntp_client.delete_address_scope(address_scope['id'])
diff --git a/patrole_tempest_plugin/tests/api/network/test_dscp_marking_rule_rbac.py b/patrole_tempest_plugin/tests/api/network/test_dscp_marking_rule_rbac.py
new file mode 100644
index 0000000..b9f8365
--- /dev/null
+++ b/patrole_tempest_plugin/tests/api/network/test_dscp_marking_rule_rbac.py
@@ -0,0 +1,106 @@
+# Copyright 2018 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 utils
+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_rule_validation
+from patrole_tempest_plugin.tests.api.network import rbac_base as base
+
+
+class DscpMarkingRulePluginRbacTest(base.BaseNetworkPluginRbacTest):
+
+    @classmethod
+    def skip_checks(cls):
+        super(DscpMarkingRulePluginRbacTest, cls).skip_checks()
+        if not utils.is_extension_enabled('qos', 'network'):
+            msg = "qos extension not enabled."
+            raise cls.skipException(msg)
+
+    @classmethod
+    def resource_setup(cls):
+        super(DscpMarkingRulePluginRbacTest, cls).resource_setup()
+        name = data_utils.rand_name(cls.__class__.__name__ + '-qos')
+        cls.policy_id = cls.ntp_client.create_qos_policy(
+            name=name)["policy"]["id"]
+        cls.addClassResourceCleanup(
+            cls.ntp_client.delete_qos_policy, cls.policy_id)
+
+    def create_policy_dscp_marking_rule(cls):
+        rule = cls.ntp_client.create_dscp_marking_rule(cls.policy_id, 10)
+        rule_id = rule['dscp_marking_rule']['id']
+        cls.addCleanup(
+            test_utils.call_and_ignore_notfound_exc,
+            cls.ntp_client.delete_dscp_marking_rule, cls.policy_id, rule_id)
+        return rule_id
+
+    @decorators.idempotent_id('2717AB75-E4CF-4CA4-AF04-5BEC0C808AA5')
+    @rbac_rule_validation.action(service="neutron",
+                                 rules=["create_policy_dscp_marking_rule"])
+    def test_create_policy_dscp_marking_rule(self):
+        """Create policy_dscp_marking_rule.
+
+        RBAC test for the neutron "create_policy_dscp_marking_rule" policy
+        """
+
+        with self.rbac_utils.override_role(self):
+            self.create_policy_dscp_marking_rule()
+
+    @decorators.idempotent_id('3D68F50E-B948-4B25-8A72-F6F4890BBC6F')
+    @rbac_rule_validation.action(service="neutron",
+                                 rules=["get_policy_dscp_marking_rule"],
+                                 expected_error_codes=[404])
+    def test_show_policy_dscp_marking_rule(self):
+        """Show policy_dscp_marking_rule.
+
+        RBAC test for the neutron "get_policy_dscp_marking_rule" policy
+        """
+        rule_id = self.create_policy_dscp_marking_rule()
+
+        with self.rbac_utils.override_role(self):
+            self.ntp_client.show_dscp_marking_rule(self.policy_id, rule_id)
+
+    @decorators.idempotent_id('33830794-8731-45C3-BC97-17718555DD7C')
+    @rbac_rule_validation.action(service="neutron",
+                                 rules=["get_policy_dscp_marking_rule",
+                                        "update_policy_dscp_marking_rule"],
+                                 expected_error_codes=[404, 403])
+    def test_update_policy_dscp_marking_rule(self):
+        """Update policy_dscp_marking_rule.
+
+        RBAC test for the neutron "update_policy_dscp_marking_rule" policy
+        """
+        rule_id = self.create_policy_dscp_marking_rule()
+
+        with self.rbac_utils.override_role(self):
+            self.ntp_client.update_dscp_marking_rule(
+                self.policy_id, rule_id, dscp_mark=16)
+
+    @decorators.idempotent_id('7BF564DD-3648-4D12-8A8B-6D5E576D1843')
+    @rbac_rule_validation.action(service="neutron",
+                                 rules=["get_policy_dscp_marking_rule",
+                                        "delete_policy_dscp_marking_rule"],
+                                 expected_error_codes=[404, 403])
+    def test_delete_policy_dscp_marking_rule(self):
+        """Delete policy_dscp_marking_rule.
+
+        RBAC test for the neutron "delete_policy_dscp_marking_rule" policy
+        """
+        rule_id = self.create_policy_dscp_marking_rule()
+
+        with self.rbac_utils.override_role(self):
+            self.ntp_client.delete_dscp_marking_rule(self.policy_id, rule_id)
diff --git a/patrole_tempest_plugin/tests/api/network/test_qos_rbac.py b/patrole_tempest_plugin/tests/api/network/test_qos_rbac.py
new file mode 100644
index 0000000..20f9e61
--- /dev/null
+++ b/patrole_tempest_plugin/tests/api/network/test_qos_rbac.py
@@ -0,0 +1,100 @@
+# Copyright 2018 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 utils
+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_rule_validation
+from patrole_tempest_plugin.tests.api.network import rbac_base as base
+
+
+class QosRbacTest(base.BaseNetworkPluginRbacTest):
+
+    @classmethod
+    def skip_checks(cls):
+        super(QosRbacTest, cls).skip_checks()
+        if not utils.is_extension_enabled('qos', 'network'):
+            msg = "qos extension not enabled."
+            raise cls.skipException(msg)
+
+    @classmethod
+    def resource_setup(cls):
+        super(QosRbacTest, cls).resource_setup()
+        cls.network = cls.create_network()
+
+    def create_policy(self, name=None):
+        name = name or data_utils.rand_name(self.__class__.__name__)
+        policy = self.ntp_client.create_qos_policy(name)['policy']
+        self.addCleanup(test_utils.call_and_ignore_notfound_exc,
+                        self.ntp_client.delete_qos_policy, policy['id'])
+        return policy
+
+    @rbac_rule_validation.action(service="neutron",
+                                 rules=["create_policy"],
+                                 expected_error_codes=[403])
+    @decorators.idempotent_id('2ade2e48-7f82-4650-a69c-933d8d594636')
+    def test_create_policy(self):
+
+        """Create Policy Test
+
+        RBAC test for the neutron create_policy policy
+        """
+        with self.rbac_utils.override_role(self):
+            self.create_policy()
+
+    @rbac_rule_validation.action(service="neutron",
+                                 rules=["get_policy"],
+                                 expected_error_codes=[404])
+    @decorators.idempotent_id('d004a8de-b226-4eb4-9fdc-8202a7f64c56')
+    def test_get_policy(self):
+
+        """Show Policy Test
+
+        RBAC test for the neutron get_policy policy
+        """
+        policy = self.create_policy()
+        with self.rbac_utils.override_role(self):
+            self.ntp_client.show_qos_policy(policy['id'])
+
+    @rbac_rule_validation.action(service="neutron",
+                                 rules=["get_policy", "update_policy"],
+                                 expected_error_codes=[404, 403])
+    @decorators.idempotent_id('fb74d56f-1dfc-490b-a9e1-454af583eefb')
+    def test_update_policy(self):
+
+        """Update Policy Test
+
+        RBAC test for the neutron update_policy policy
+        """
+        policy = self.create_policy()
+        with self.rbac_utils.override_role(self):
+            self.ntp_client.update_qos_policy(policy['id'],
+                                              description='updated')
+
+    @rbac_rule_validation.action(service="neutron",
+                                 rules=["get_policy", "delete_policy"],
+                                 expected_error_codes=[404, 403])
+    @decorators.idempotent_id('ef4c23a6-4095-47a6-958e-1df585f7d8db')
+    def test_delete_policy(self):
+
+        """Delete Policy Test
+
+        RBAC test for the neutron delete_policy policy
+        """
+        policy = self.create_policy()
+        with self.rbac_utils.override_role(self):
+            self.ntp_client.delete_qos_policy(policy['id'])
diff --git a/patrole_tempest_plugin/tests/api/network/test_subnetpools_rbac.py b/patrole_tempest_plugin/tests/api/network/test_subnetpools_rbac.py
index 7d02271..62735d7 100644
--- a/patrole_tempest_plugin/tests/api/network/test_subnetpools_rbac.py
+++ b/patrole_tempest_plugin/tests/api/network/test_subnetpools_rbac.py
@@ -65,7 +65,33 @@
 
     @rbac_rule_validation.action(service="neutron",
                                  rules=["create_subnetpool",
-                                        "create_subnetpool:shared"])
+                                        "create_subnetpool:is_default"],
+                                 expected_error_codes=[403, 403])
+    @decorators.idempotent_id('1b5509fd-2c32-44a8-a786-1b6ca162dbd2')
+    def test_create_subnetpool_default(self):
+        """Create default subnetpool.
+
+        RBAC test for the neutron create_subnetpool:is_default policy
+        """
+        # Most likely we already have default subnetpools for ipv4 and ipv6,
+        # so we temporary mark them as is_default=False, to let this test pass.
+        def_pools = self.subnetpools_client.list_subnetpools(is_default=True)
+        for default_pool in def_pools["subnetpools"]:
+            self.subnetpools_client.update_subnetpool(default_pool["id"],
+                                                      is_default=False)
+
+            self.addCleanup(self.subnetpools_client.update_subnetpool,
+                            default_pool["id"], is_default=True)
+
+        with self.rbac_utils.override_role(self):
+            # It apparently only enforces the policy for is_default=True.
+            # It does nothing for is_default=False
+            self._create_subnetpool(is_default=True)
+
+    @rbac_rule_validation.action(service="neutron",
+                                 rules=["create_subnetpool",
+                                        "create_subnetpool:shared"],
+                                 expected_error_codes=[403, 403])
     @decorators.idempotent_id('cf730989-0d47-40bc-b39a-99e7de484723')
     def test_create_subnetpool_shared(self):
         """Create subnetpool shared.
diff --git a/patrole_tempest_plugin/tests/api/volume/rbac_base.py b/patrole_tempest_plugin/tests/api/volume/rbac_base.py
index 8fa3b67..14b3151 100644
--- a/patrole_tempest_plugin/tests/api/volume/rbac_base.py
+++ b/patrole_tempest_plugin/tests/api/volume/rbac_base.py
@@ -29,11 +29,6 @@
     _api_version = 3
 
     @classmethod
-    def skip_checks(cls):
-        super(BaseVolumeRbacTest, cls).skip_checks()
-        cls.skip_rbac_checks()
-
-    @classmethod
     def setup_clients(cls):
         super(BaseVolumeRbacTest, cls).setup_clients()
         cls.setup_rbac_utils()
diff --git a/patrole_tempest_plugin/tests/api/volume/test_encryption_types_rbac.py b/patrole_tempest_plugin/tests/api/volume/test_encryption_types_rbac.py
index f10e41b..2ee80eb 100644
--- a/patrole_tempest_plugin/tests/api/volume/test_encryption_types_rbac.py
+++ b/patrole_tempest_plugin/tests/api/volume/test_encryption_types_rbac.py
@@ -13,12 +13,36 @@
 #    License for the specific language governing permissions and limitations
 #    under the License.
 
+import functools
+
 from tempest.common import utils
+from tempest import config
 from tempest.lib import decorators
 
 from patrole_tempest_plugin import rbac_rule_validation
 from patrole_tempest_plugin.tests.api.volume import rbac_base
 
+CONF = config.CONF
+
+
+def _get_volume_type_encryption_policy(action):
+    feature_flag = CONF.policy_feature_enabled.added_cinder_policies_stein
+
+    if feature_flag:
+        return "volume_extension:volume_type_encryption:%s" % action
+
+    return "volume_extension:volume_type_encryption"
+
+
+_CREATE_VOLUME_TYPE_ENCRYPTION = functools.partial(
+    _get_volume_type_encryption_policy, "create")
+_SHOW_VOLUME_TYPE_ENCRYPTION = functools.partial(
+    _get_volume_type_encryption_policy, "get")
+_UPDATE_VOLUME_TYPE_ENCRYPTION = functools.partial(
+    _get_volume_type_encryption_policy, "update")
+_DELETE_VOLUME_TYPE_ENCRYPTION = functools.partial(
+    _get_volume_type_encryption_policy, "delete")
+
 
 class EncryptionTypesV3RbacTest(rbac_base.BaseVolumeRbacTest):
 
@@ -45,7 +69,7 @@
     @decorators.idempotent_id('ffd94ce5-c24b-4b6c-84c9-c5aad8c3010c')
     @rbac_rule_validation.action(
         service="cinder",
-        rule="volume_extension:volume_type_encryption")
+        rule=_CREATE_VOLUME_TYPE_ENCRYPTION)
     def test_create_volume_type_encryption(self):
         vol_type_id = self.create_volume_type()['id']
         with self.rbac_utils.override_role(self):
@@ -57,7 +81,7 @@
     @decorators.idempotent_id('6599e72e-acef-4c0d-a9b2-463fca30d1da')
     @rbac_rule_validation.action(
         service="cinder",
-        rule="volume_extension:volume_type_encryption")
+        rule=_DELETE_VOLUME_TYPE_ENCRYPTION)
     def test_delete_volume_type_encryption(self):
         vol_type_id = self._create_volume_type_encryption()
         with self.rbac_utils.override_role(self):
@@ -66,7 +90,7 @@
     @decorators.idempotent_id('42da9fec-32fd-4dca-9242-8a53b2fed25a')
     @rbac_rule_validation.action(
         service="cinder",
-        rule="volume_extension:volume_type_encryption")
+        rule=_UPDATE_VOLUME_TYPE_ENCRYPTION)
     def test_update_volume_type_encryption(self):
         vol_type_id = self._create_volume_type_encryption()
         with self.rbac_utils.override_role(self):
@@ -77,7 +101,7 @@
     @decorators.idempotent_id('1381a3dc-248f-4282-b231-c9399018c804')
     @rbac_rule_validation.action(
         service="cinder",
-        rule="volume_extension:volume_type_encryption")
+        rule=_SHOW_VOLUME_TYPE_ENCRYPTION)
     def test_show_volume_type_encryption(self):
         vol_type_id = self._create_volume_type_encryption()
         with self.rbac_utils.override_role(self):
@@ -86,7 +110,7 @@
     @decorators.idempotent_id('d4ed3cf8-52b2-4fa2-910d-e405361f0881')
     @rbac_rule_validation.action(
         service="cinder",
-        rule="volume_extension:volume_type_encryption")
+        rule=_SHOW_VOLUME_TYPE_ENCRYPTION)
     def test_show_encryption_specs_item(self):
         vol_type_id = self._create_volume_type_encryption()
         with self.rbac_utils.override_role(self):
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 fe36f2c..1772047 100644
--- a/patrole_tempest_plugin/tests/unit/test_rbac_rule_validation.py
+++ b/patrole_tempest_plugin/tests/unit/test_rbac_rule_validation.py
@@ -14,6 +14,7 @@
 
 from __future__ import absolute_import
 
+import functools
 import mock
 from oslo_config import cfg
 
@@ -80,7 +81,6 @@
             pass
 
         test_policy(self.mock_test_args)
-        mock_log.warning.assert_not_called()
         mock_log.error.assert_not_called()
 
     @mock.patch.object(rbac_rv, 'LOG', autospec=True)
@@ -99,7 +99,6 @@
             raise exceptions.Forbidden()
 
         test_policy(self.mock_test_args)
-        mock_log.warning.assert_not_called()
         mock_log.error.assert_not_called()
 
     @mock.patch.object(rbac_rv, 'LOG', autospec=True)
@@ -130,7 +129,8 @@
     @mock.patch.object(rbac_rv, 'policy_authority', autospec=True)
     def test_rule_validation_rbac_malformed_response_positive(
             self, mock_authority, mock_log):
-        """Test RbacMalformedResponse error is thrown without permission passes.
+        """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.
@@ -143,7 +143,6 @@
             raise rbac_exceptions.RbacMalformedResponse()
 
         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)
@@ -171,7 +170,8 @@
     @mock.patch.object(rbac_rv, 'policy_authority', autospec=True)
     def test_rule_validation_rbac_conflicting_policies_positive(
             self, mock_authority, mock_log):
-        """Test RbacConflictingPolicies error is thrown without permission passes.
+        """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.
@@ -184,7 +184,6 @@
             raise rbac_exceptions.RbacConflictingPolicies()
 
         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)
@@ -448,6 +447,66 @@
             "Allowed",
             "Allowed")
 
+    @mock.patch.object(rbac_rv, 'LOG', autospec=True)
+    @mock.patch.object(rbac_rv, 'policy_authority', autospec=True)
+    def test_rule_validation_with_callable_rule(self, mock_authority,
+                                                mock_log):
+        """Test that a callable as the rule is evaluated correctly."""
+        mock_authority.PolicyAuthority.return_value.allowed.return_value = True
+
+        @rbac_rv.action(mock.sentinel.service,
+                        rule=lambda: mock.sentinel.action)
+        def test_policy(*args):
+            pass
+
+        test_policy(self.mock_test_args)
+
+        policy_authority = mock_authority.PolicyAuthority.return_value
+        policy_authority.allowed.assert_called_with(
+            mock.sentinel.action,
+            CONF.patrole.rbac_test_role)
+
+        mock_log.error.assert_not_called()
+
+    @mock.patch.object(rbac_rv, 'LOG', autospec=True)
+    @mock.patch.object(rbac_rv, 'policy_authority', autospec=True)
+    def test_rule_validation_with_conditional_callable_rule(
+            self, mock_authority, mock_log):
+        """Test that a complex callable with conditional logic as the rule is
+        evaluated correctly.
+        """
+        mock_authority.PolicyAuthority.return_value.allowed.return_value = True
+
+        def partial_func(x):
+            return "foo" if x == "bar" else "qux"
+        foo_callable = functools.partial(partial_func, "bar")
+        bar_callable = functools.partial(partial_func, "baz")
+
+        @rbac_rv.action(mock.sentinel.service,
+                        rule=foo_callable)
+        def test_foo_policy(*args):
+            pass
+
+        @rbac_rv.action(mock.sentinel.service,
+                        rule=bar_callable)
+        def test_bar_policy(*args):
+            pass
+
+        test_foo_policy(self.mock_test_args)
+        policy_authority = mock_authority.PolicyAuthority.return_value
+        policy_authority.allowed.assert_called_with(
+            "foo",
+            CONF.patrole.rbac_test_role)
+        policy_authority.allowed.reset_mock()
+
+        test_bar_policy(self.mock_test_args)
+        policy_authority = mock_authority.PolicyAuthority.return_value
+        policy_authority.allowed.assert_called_with(
+            "qux",
+            CONF.patrole.rbac_test_role)
+
+        mock_log.error.assert_not_called()
+
 
 class RBACRuleValidationNegativeTest(BaseRBACRuleValidationTest):
 
diff --git a/patrole_tempest_plugin/tests/unit/test_rbac_utils.py b/patrole_tempest_plugin/tests/unit/test_rbac_utils.py
index c5264aa..5132079 100644
--- a/patrole_tempest_plugin/tests/unit/test_rbac_utils.py
+++ b/patrole_tempest_plugin/tests/unit/test_rbac_utils.py
@@ -208,11 +208,6 @@
         class FakeRbacTest(rbac_utils.RbacUtilsMixin, test.BaseTestCase):
 
             @classmethod
-            def skip_checks(cls):
-                super(FakeRbacTest, cls).skip_checks()
-                cls.skip_rbac_checks()
-
-            @classmethod
             def setup_clients(cls):
                 super(FakeRbacTest, cls).setup_clients()
                 cls.setup_rbac_utils()
@@ -237,21 +232,3 @@
 
         self.assertTrue(hasattr(child_test, 'rbac_utils'))
         self.assertIsInstance(child_test.rbac_utils, rbac_utils.RbacUtils)
-
-    def test_skip_rbac_checks(self):
-        """Validate that the child class is skipped if `[patrole] enable_rbac`
-        is False and that the child class's name is in the skip message.
-        """
-        self.useFixture(patrole_fixtures.ConfPatcher(enable_rbac=False,
-                                                     group='patrole'))
-
-        class ChildRbacTest(self.parent_class):
-            pass
-
-        child_test = ChildRbacTest()
-
-        with testtools.ExpectedException(
-                testtools.TestCase.skipException,
-                value_re=('Patrole testing not enabled so skipping %s.'
-                          % ChildRbacTest.__name__)):
-            child_test.setUpClass()
diff --git a/releasenotes/notes/remove-deprecated-enable-rbac-config-option-a5e46ce1053b7dea.yaml b/releasenotes/notes/remove-deprecated-enable-rbac-config-option-a5e46ce1053b7dea.yaml
new file mode 100644
index 0000000..53b1710
--- /dev/null
+++ b/releasenotes/notes/remove-deprecated-enable-rbac-config-option-a5e46ce1053b7dea.yaml
@@ -0,0 +1,5 @@
+---
+upgrade:
+  - |
+    Remove deprecated ``[patrole].enable_rbac`` configuration option. To skip
+    Patrole tests going forward, use an appropriate regex.
diff --git a/releasenotes/notes/volume-type-encryption-policy-granularity-141ac283b9c0778e.yaml b/releasenotes/notes/volume-type-encryption-policy-granularity-141ac283b9c0778e.yaml
new file mode 100644
index 0000000..4aeb107
--- /dev/null
+++ b/releasenotes/notes/volume-type-encryption-policy-granularity-141ac283b9c0778e.yaml
@@ -0,0 +1,19 @@
+---
+features:
+  - |
+    Added new Cinder feature flag (``CONF.policy_feature_enabled.added_cinder_policies_stein``)
+    for the following newly introduced granular Cinder policies:
+
+    - ``volume_extension:volume_type_encryption:create``
+    - ``volume_extension:volume_type_encryption:get``
+    - ``volume_extension:volume_type_encryption:update``
+    - ``volume_extension:volume_type_encryption:delete``
+
+    The corresponding Patrole test cases are modified to support
+    the granularity.  The test cases also support backward
+    compatibility with the old single rule:
+    ``volume_extension:volume_type_encryption``
+
+    The ``rules`` parameter in ``rbac_rule_validation.action``
+    decorator now also accepts a list of callables; each callable
+    should return a policy action (str).