Merge "docs: Use sphinx-apidoc library for autodoc generation"
diff --git a/.zuul.yaml b/.zuul.yaml
index fb110f0..b2b59a2 100644
--- a/.zuul.yaml
+++ b/.zuul.yaml
@@ -10,11 +10,14 @@
     timeout: 7800
     roles:
       - zuul: openstack-dev/devstack
-    irrelevant-files:
+    # Define common irrelevant files to use everywhere else
+    irrelevant-files: &patrole-irrelevant-files
       - ^(test-|)requirements.txt$
       - ^.*\.rst$
       - ^doc/.*
-      - ^patrole/patrole_tempest_plugin/tests/unit/.*$
+      - ^etc/.*$
+      - ^patrole_tempest_plugin/tests/unit/.*$
+      - ^patrole_tempest_plugin/hacking/.*$
       - ^releasenotes/.*
       - ^setup.cfg$
     vars:
@@ -28,7 +31,8 @@
         neutron-trunk: true
       tempest_concurrency: 2
       tempest_test_regex: (?!.*\[.*\bslow\b.*\])(^patrole_tempest_plugin\.tests\.api)
-      tox_envlist: all-plugin
+      tox_envlist: all
+      tox_extra_args: --sitepackages
 
 - job:
     name: patrole-base-multinode
@@ -46,13 +50,7 @@
       - openstack-infra/devstack-gate
       - openstack/tempest
       - openstack/patrole
-    irrelevant-files:
-      - ^(test-|)requirements.txt$
-      - ^.*\.rst$
-      - ^doc/.*
-      - ^patrole/patrole_tempest_plugin/tests/unit/.*$
-      - ^releasenotes/.*
-      - ^setup.cfg$
+    irrelevant-files: *patrole-irrelevant-files
     vars:
       devstack_localrc:
         TEMPEST_PLUGINS: "'{{ ansible_user_dir }}/src/git.openstack.org/openstack/patrole'"
@@ -63,7 +61,8 @@
         neutron: true
       tempest_concurrency: 1
       tempest_test_regex: (?=.*\[.*\bslow\b.*\])(^patrole_tempest_plugin\.tests\.api)
-      tox_envlist: all-plugin
+      tox_envlist: all
+      tox_extra_args: --sitepackages
 
 - job:
     name: patrole-admin
@@ -71,7 +70,7 @@
     description: Patrole job for admin role.
     vars:
       devstack_localrc:
-        RBAC_TEST_ROLE: admin
+        RBAC_TEST_ROLES: admin
 
 - job:
     name: patrole-member
@@ -84,7 +83,7 @@
       - stable/pike
     vars:
       devstack_localrc:
-        RBAC_TEST_ROLE: member
+        RBAC_TEST_ROLES: member
 
 - job:
     name: patrole-member-rocky
@@ -107,7 +106,7 @@
     voting: false
     vars:
       devstack_localrc:
-        RBAC_TEST_ROLE: admin
+        RBAC_TEST_ROLES: admin
 
 - job:
     name: patrole-multinode-member
@@ -115,7 +114,7 @@
     voting: false
     vars:
       devstack_localrc:
-        RBAC_TEST_ROLE: member
+        RBAC_TEST_ROLES: member
 
 - job:
     name: patrole-py35-member
@@ -125,7 +124,7 @@
       devstack_localrc:
         # Use member for py35 because arguably negative testing is more
         # important than admin, which is already covered by patrole-admin job.
-        RBAC_TEST_ROLE: member
+        RBAC_TEST_ROLES: member
         USE_PYTHON3: true
       devstack_services:
         s-account: false
@@ -136,11 +135,14 @@
         c-bak: false
 
 - job:
-    name: patrole-plugin-base
+    name: patrole-extension-base
     parent: patrole-base
     description: |
-      Patrole plugin job for admin and member roles which
-      runs RBAC tests for neutron-tempest-plugin APIs (if the plugin is installed).
+      Patrole plugin job for admin and member roles which runs RBAC tests for
+      neutron-tempest-plugin APIs (if the plugin is installed).
+
+      Covers Neutron extension functionality only. Should not be used for
+      supporting Neutron plugins like fwaas.
     required-projects:
       - name: openstack/tempest
       - name: openstack/patrole
@@ -159,22 +161,22 @@
         neutron-qos: true
 
 - job:
-    name: patrole-plugin-member
-    parent: patrole-plugin-base
+    name: patrole-extension-member
+    parent: patrole-extension-base
     voting: false
     vars:
       devstack_localrc:
-        RBAC_TEST_ROLE: member
-      tempest_test_regex: (?=.*PluginRbacTest)(^patrole_tempest_plugin\.tests\.api)
+        RBAC_TEST_ROLES: member
+      tempest_test_regex: (?=.*ExtRbacTest)(^patrole_tempest_plugin\.tests\.api)
 
 - job:
-    name: patrole-plugin-admin
-    parent: patrole-plugin-base
+    name: patrole-extension-admin
+    parent: patrole-extension-base
     voting: false
     vars:
       devstack_localrc:
-        RBAC_TEST_ROLE: admin
-      tempest_test_regex: (?=.*PluginRbacTest)(^patrole_tempest_plugin\.tests\.api)
+        RBAC_TEST_ROLES: admin
+      tempest_test_regex: (?=.*ExtRbacTest)(^patrole_tempest_plugin\.tests\.api)
 
 - project:
     templates:
@@ -196,8 +198,8 @@
         - patrole-py35-member
         - patrole-multinode-admin
         - patrole-multinode-member
-        - patrole-plugin-admin
-        - patrole-plugin-member
+        - patrole-extension-admin
+        - patrole-extension-member
     gate:
       jobs:
         - patrole-admin
diff --git a/HACKING.rst b/HACKING.rst
index 87e3b1f..9868e39 100644
--- a/HACKING.rst
+++ b/HACKING.rst
@@ -39,9 +39,9 @@
 - [P102] RBAC test class names must end in 'RbacTest'
 - [P103] ``self.client`` must not be used as a client alias; this allows for
   code that is more maintainable and easier to read
-- [P104] RBAC `plugin test class`_ names must end in 'PluginRbacTest'
+- [P104] RBAC `extension test class`_ names must end in 'ExtRbacTest'
 
-.. _plugin test class: https://github.com/openstack/patrole/tree/master/patrole_tempest_plugin/tests/api/network#neutron-plugin-tests
+.. _extension test class: https://github.com/openstack/patrole/tree/master/patrole_tempest_plugin/tests/api/network#neutron-extension-rbac-tests
 
 Role Overriding
 ---------------
@@ -121,3 +121,34 @@
 policies also applies to Patrole.
 
 .. _Tempest logic: https://docs.openstack.org/tempest/latest/HACKING.html#new-tests-for-existing-features
+
+
+Black Box vs. White Box Testing
+-------------------------------
+
+Tempest is a `black box testing framework`_, meaning that it is concerned with
+testing public API endpoints and doesn't concern itself with testing internal
+implementation details. Patrole, as a Tempest plugin, also falls underneath
+the category of black box testing. However, even with policy in code
+documentation, some degree of white box testing is required in order to
+correctly write RBAC tests.
+
+This is because :ref:`policy-in-code` documentation, while useful in many
+respects, is usually quite brief and its main purpose is to help operators
+understand how to customize policy configuration rather than to help
+developers understand complex policy authorization work flows. For example,
+policy in code documentation doesn't make deriving
+:ref:`multiple policies <multiple-policies>` easy. Such documentation also
+doesn't usually mention that a specific parameter needs to be set, or that a
+particular microversion must be enabled, or that a particular set of
+prerequisite API or policy actions must be executed, in order for the policy
+under test to be enforced by the server. This means that test writers must
+account for the internal RBAC implementation in API code in order to correctly
+understand the complete RBAC work flow within an API.
+
+Besides, as mentioned :ref:`elsewhere <design-principles>` in this
+documentation, not all services currently implement policy in code, making
+some degree of white box testing a "necessary evil" for writing robust RBAC
+tests.
+
+.. _black box testing framework: https://docs.openstack.org/tempest/latest/HACKING.html#negative-tests
diff --git a/README.rst b/README.rst
index fdcbc6b..5331445 100644
--- a/README.rst
+++ b/README.rst
@@ -153,10 +153,11 @@
 
    will run the same set of tests as the default gate jobs.
 
-   You can also run Patrole tests using `tox`_. To do so, ``cd`` into the
+   You can also run Patrole tests using `tox`_, but as Patrole needs access to
+   global packages use ``--sitepackages`` argument. To do so, ``cd`` into the
    **Tempest** directory and run::
 
-     $ tox -eall-plugin -- patrole_tempest_plugin.tests.api
+     $ tox -eall --sitepackages -- patrole_tempest_plugin.tests.api
 
    .. note::
 
@@ -186,23 +187,25 @@
 RBAC Tests
 ----------
 
-To change the role that the patrole tests are being run as, edit
-``rbac_test_role`` in the ``patrole`` section of tempest.conf: ::
+To change the roles that the patrole tests are being run as, edit
+``rbac_test_roles`` in the ``patrole`` section of tempest.conf: ::
 
     [patrole]
-    rbac_test_role = member
+    rbac_test_role = member,reader
     ...
 
 .. note::
 
-  The ``rbac_test_role`` is service-specific. member, for example,
+  The ``rbac_test_roles`` is service-specific. member, for example,
   is an arbitrary role, but by convention is used to designate the default
   non-admin role in the system. Most Patrole tests should be run with
   **admin** and **member** roles. However, other services may use entirely
-  different roles.
+  different roles or role combinations.
 
-For more information about the member role and its nomenclature,
-please see: `<https://ask.openstack.org/en/question/4759/member-vs-_member_/>`__.
+For more information about RBAC, reference the `rbac-overview`_
+documentation page.
+
+.. _rbac-overview: https://docs.openstack.org/patrole/latest/rbac-overview.html
 
 Unit Tests
 ----------
diff --git a/REVIEWING.rst b/REVIEWING.rst
index 7e3c71f..4ee847f 100644
--- a/REVIEWING.rst
+++ b/REVIEWING.rst
@@ -109,6 +109,58 @@
 whether to skip or not.
 
 
+Multi-Policy Guidelines
+-----------------------
+
+Care should be taken when using multiple policies in an RBAC test. The
+following guidelines should be followed before deciding to add multiple
+policies to a Patrole test.
+
+.. _general-multi-policy-guidelines:
+
+General Multi-policy API Code Guidelines
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+The list below enumerates guidelines beginning with those with the highest
+priority and ending with those with the lowest priority. A higher priority
+item takes precedence over lower priority items.
+
+#. Order the policies in the ``rules`` parameter chronologically with respect
+   to the order they're called by the API endpoint under test.
+#. Only use policies that map to the API by referencing the appropriate policy
+   in code documentation.
+#. Only include the minimum number of policies needed to test the API
+   correctly: don't add extraneous policies.
+#. If possible, only use policies that directly relate to the API. If the
+   policies are used across multiple APIs, try to omit it. If a "generic"
+   policy needs to be added to get the test to pass, then this is fair game.
+#. Limit the number of policies to a reasonable number, such as 3.
+
+Neutron Multi-policy API Code Guidelines
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Because Neutron can raise a 403 or 404 following failed authorization, Patrole
+uses the ``expected_error_codes`` parameter to accommodate this behavior.
+Each policy action enumerated in ``rules`` must have a corresponding entry
+in ``expected_error_codes``. Each expected error code must be either a 403 or a
+404, which indicates that, when policy enforcement fails for the corresponding
+policy action, that error code is expected by Patrole. For more information
+about these parameters, see :ref:`rbac-validation`.
+
+The list below enumerates additional multi-policy guidelines that apply in
+particular to Neutron. A higher priority item takes precedence over lower
+priority items.
+
+#. Order the expected error codes in the ``expected_error_codes`` parameter
+   chronologically with respect to the order each corresponding policy in
+   ``rules`` is authorized by the API under test.
+#. Ensure the :ref:`neutron-multi-policy-validation` is followed when
+   determining the expected error code for each corresponding policy.
+
+The same guidelines under :ref:`general-multi-policy-guidelines` should be
+applied afterward.
+
+
 Release Notes
 -------------
 Release notes are how we indicate to users and other consumers of Patrole what
diff --git a/devstack/plugin.sh b/devstack/plugin.sh
index 502b68c..6b95182 100644
--- a/devstack/plugin.sh
+++ b/devstack/plugin.sh
@@ -14,9 +14,14 @@
     setup_package $PATROLE_DIR -e
 
     if [[ ${DEVSTACK_SERIES} == 'pike' ]]; then
-        if [[ "$RBAC_TEST_ROLE" == "member" ]]; then
-            RBAC_TEST_ROLE="Member"
-        fi
+        IFS=',' read -ra roles_array <<< "$RBAC_TEST_ROLES"
+        RBAC_TEST_ROLES=""
+        for i in "${roles_array[@]}"; do
+            if [[ $i == "member" ]]; then
+                i="Member"
+            fi
+            RBAC_TEST_ROLES="$i,$RBAC_TEST_ROLES"
+        done
 
         # Policies used by Patrole testing that were changed in a backwards-incompatible way.
         # TODO(felipemonteiro): Remove these once stable/pike becomes EOL.
@@ -35,9 +40,14 @@
     fi
 
     if [[ ${DEVSTACK_SERIES} == 'queens' ]]; then
-        if [[ "$RBAC_TEST_ROLE" == "member" ]]; then
-            RBAC_TEST_ROLE="Member"
-        fi
+        IFS=',' read -ra roles_array <<< "$RBAC_TEST_ROLES"
+        RBAC_TEST_ROLES=""
+        for i in "${roles_array[@]}"; do
+            if [[ $i == "member" ]]; then
+                i="Member"
+            fi
+            RBAC_TEST_ROLES="$i,$RBAC_TEST_ROLES"
+        done
 
         # TODO(cl566n): Remove these once stable/queens becomes EOL.
         # These policies were removed in Stein but are available in Queens.
@@ -52,7 +62,7 @@
         iniset $TEMPEST_CONFIG policy-feature-enabled removed_keystone_policies_stein False
     fi
 
-    iniset $TEMPEST_CONFIG patrole rbac_test_role $RBAC_TEST_ROLE
+    iniset $TEMPEST_CONFIG patrole rbac_test_roles $RBAC_TEST_ROLES
 }
 
 if is_service_enabled tempest; then
diff --git a/doc/source/configuration.rst b/doc/source/configuration.rst
index f6aaf04..05716fe 100644
--- a/doc/source/configuration.rst
+++ b/doc/source/configuration.rst
@@ -7,34 +7,28 @@
 file. All Patrole-specific configuration options should be included under
 the ``patrole`` group.
 
-RBAC Test Role
---------------
+RBAC Test Roles
+---------------
 
-The RBAC test role governs which role is used when running Patrole tests. For
-example, setting ``rbac_test_role`` to "admin" will execute all RBAC tests
-using admin credentials. Changing the ``rbac_test_role`` value will `override`
-Tempest's primary credentials to use that role.
+The RBAC test roles govern the list of roles to be used when running Patrole
+tests. For example, setting ``rbac_test_roles`` to "admin" will execute all
+RBAC tests using admin credentials. Changing the ``rbac_test_roles`` value
+will `override` Tempest's primary credentials to use that role.
 
-This implies that, if ``rbac_test_role`` is "admin", regardless of the Tempest
+This implies that, if ``rbac_test_roles`` is "admin", regardless of the Tempest
 credentials used by a client, the client will be calling APIs using the admin
 role. That is, ``self.os_primary.servers_client`` will run as though it were
 ``self.os_admin.servers_client``.
 
-Similarly, setting ``rbac_test_role`` to a non-admin role results in Tempest's
-primary credentials being overridden by the role specified by
-``rbac_test_role``.
+Similarly, setting ``rbac_test_roles`` with various roles, results in
+Tempest's primary credentials being overridden by the roles specified by
+``rbac_test_roles``.
 
 .. note::
 
-    Only the role of the primary Tempest credentials ("os_primary") is
+    Only the roles of the primary Tempest credentials ("os_primary") are
     modified. The ``user_id`` and ``project_id`` remain unchanged.
 
-Enable RBAC
------------
-
-Given the value of ``enable_rbac``, enables or disables Patrole tests. If
-``enable_rbac`` is ``False``, then Patrole tests are skipped.
-
 Custom Policy Files
 -------------------
 
diff --git a/doc/source/framework/overview.rst b/doc/source/framework/overview.rst
index 8e04082..6f72eec 100644
--- a/doc/source/framework/overview.rst
+++ b/doc/source/framework/overview.rst
@@ -10,10 +10,10 @@
 RBAC testing validation is broken up into 3 stages:
 
 #. "Expected" stage. Determine whether the test should be able to succeed
-   or fail based on the test role defined by ``[patrole] rbac_test_role``)
+   or fail based on the test roles defined by ``[patrole] rbac_test_roles``)
    and the policy action that the test enforces.
 #. "Actual" stage. Run the test by calling the API endpoint that enforces
-   the expected policy action using the test role.
+   the expected policy action using the test roles.
 #. Comparing the outputs from both stages for consistency. A "consistent"
    result is treated as a pass and an "inconsistent" result is treated
    as a failure. "Consistent" (or successful) cases include:
@@ -63,7 +63,7 @@
 ---------------------------
 
 Module called by :ref:`rbac-validation` to verify whether the test
-role is allowed to execute a policy action by querying ``oslo.policy`` with
+roles are allowed to execute a policy action by querying ``oslo.policy`` with
 required test data. The result is used by :ref:`rbac-validation` as the
 "Expected" result.
 
diff --git a/doc/source/framework/requirements_authority.rst b/doc/source/framework/requirements_authority.rst
index dec6267..daed319 100644
--- a/doc/source/framework/requirements_authority.rst
+++ b/doc/source/framework/requirements_authority.rst
@@ -7,8 +7,9 @@
 --------
 
 Requirements-driven approach to declaring the expected RBAC test results
-referenced by Patrole. Uses a high-level YAML syntax to crystallize policy
-requirements concisely and unambiguously.
+referenced by Patrole. These requirements express the *intention* behind the
+policy. A high-level YAML syntax is used to concisely and clearly map each
+policy action to the list of associated roles.
 
 .. note::
 
@@ -29,10 +30,6 @@
 :ref:`custom-requirements-file` which precisely defines the cloud's RBAC
 requirements.
 
-Using a high-level declarative language, the requirements are captured
-unambiguously in the :ref:`custom-requirements-file`, allowing operators to
-validate their requirements against their OpenStack cloud.
-
 This validation approach should be used when:
 
 * The cloud has heavily customized policy files that require careful validation
@@ -71,31 +68,87 @@
 file must be located on the same host that Patrole runs on. The YAML
 file should be written as follows:
 
+  .. code-block:: yaml
+
+      <service_foo>:
+        <logical_or_example>:
+          - <allowed_role_1>
+          - <allowed_role_2>
+        <logical_and_example>:
+          - <allowed_role_3>, <allowed_role_4>
+      <service_bar>:
+        <logical_not_example>:
+          - <!disallowed_role_5>
+
+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 ``oslo.policy`` role that is allowed to perform the
+  API.
+
+Each item under ``logical_or_example`` is "logical OR"-ed together. Each role
+in the comma-separated string under ``logical_and_example`` is "logical AND"-ed
+together. And each item prefixed with "!" under ``logical_not_example`` is
+"logical negated".
+
+.. note::
+
+  The custom requirements file only allows policy actions to be mapped to
+  the associated roles that define it. Complex ``oslo.policy`` constructs
+  like ``literals`` or ``GenericChecks`` are not supported. For more
+  information, reference the `oslo.policy documentation`_.
+
+.. _oslo.policy documentation: https://docs.openstack.org/oslo.policy/latest/reference/api/oslo_policy.policy.html#policy-rule-expressions
+
+Examples
+~~~~~~~~
+
+Items within ``api_action`` are considered as logical or, so you may read:
+
 .. code-block:: yaml
 
     <service_foo>:
+      # "api_action_a: allowed_role_1 or allowed_role_2 or allowed_role_3"
       <api_action_a>:
         - <allowed_role_1>
         - <allowed_role_2>
         - <allowed_role_3>
-      <api_action_b>:
-        - <allowed_role_2>
-        - <allowed_role_4>
-    <service_bar>:
-      <api_action_c>:
+
+as ``<allowed_role_1> or <allowed_role_2> or <allowed_role_3>``.
+
+Roles within comma-separated items are considered as logic and, so you may
+read:
+
+.. code-block:: yaml
+
+    <service_foo>:
+      # "api_action_a: (allowed_role_1 and allowed_role_2) or allowed_role_3"
+      <api_action_a>:
+        - <allowed_role_1>, <allowed_role_2>
         - <allowed_role_3>
 
-Where:
+as ``<allowed_role_1> and <allowed_role_2> or <allowed_role_3>``.
 
-service = the service that is being tested (Cinder, Nova, etc.).
+Also negative roles may be defined with an exclamation mark ahead of role:
 
-api_action = the policy action that is being tested. Examples:
+.. code-block:: yaml
 
-* volume:create
-* os_compute_api:servers:start
-* add_image
+    <service_foo>:
+      # "api_action_a: (allowed_role_1 and allowed_role_2 and not
+      # disallowed_role_4) or allowed_role_3"
+      <api_action_a>:
+        - <allowed_role_1>, <allowed_role_2>, !<disallowed_role_4>
+        - <allowed_role_3>
 
-allowed_role = the ``oslo.policy`` role that is allowed to perform the API.
+This example must be read as ``<allowed_role_1> and <allowed_role_2> and not
+<disallowed_role_4> or <allowed_role_3>``.
+
 
 Implementation
 --------------
diff --git a/doc/source/index.rst b/doc/source/index.rst
index 8837f2e..816908a 100644
--- a/doc/source/index.rst
+++ b/doc/source/index.rst
@@ -17,6 +17,7 @@
    :maxdepth: 2
 
    rbac-overview
+   multi-policy-validation
 
 User's Guide
 ============
diff --git a/doc/source/multi-policy-validation.rst b/doc/source/multi-policy-validation.rst
new file mode 100644
index 0000000..d38b31e
--- /dev/null
+++ b/doc/source/multi-policy-validation.rst
@@ -0,0 +1,187 @@
+.. _multi-policy-validation:
+
+=======================
+Multi-policy Validation
+=======================
+
+Introduction
+------------
+
+Multi-policy validation exists in Patrole because if one policy were assumed,
+then tests could fail because they would not consider all the policies actually
+being enforced. The reasoning can be found in `this spec`_. Basically,
+since Patrole derives the expected test result dynamically in order to test any
+role, each policy enforced by the API under test must be considered to derive
+an accurate expected test result, or else the expected and actual test
+results will not always match, resulting in overall test failure. For more
+information about Patrole's RBAC validation work flow, reference
+:ref:`rbac-validation`.
+
+Multi-policy support allows Patrole to more accurately offer RBAC tests for API
+endpoints that enforce multiple policy actions.
+
+.. _this spec: https://github.com/openstack/qa-specs/blob/master/specs/patrole/rbac-testing-multiple-policies.rst
+
+Scope
+-----
+
+Multiple policies should be applied only to tests that require them. Not all
+API endpoints enforce multiple policies. Some services consistently enforce
+1 policy per API, while on the other side of the spectrum, services like
+Neutron have much more involved policy enforcement work flows. See
+:ref:`neutron-multi-policy-validation` for more information.
+
+.. _neutron-multi-policy-validation:
+
+Neutron Multi-policy Validation
+-------------------------------
+
+Neutron can raise different :ref:`policy-error-codes` following failed policy
+authorization. Many endpoints in Neutron enforce multiple policies, which
+complicates matters when trying to determine whether the endpoint raises a
+403 or a 404 following unauthorized access.
+
+Multi-policy Examples
+---------------------
+
+General Examples
+^^^^^^^^^^^^^^^^
+
+Below is an example of multi-policy validation for a carefully chosen Nova API:
+
+.. code-block:: python
+
+  @rbac_rule_validation.action(
+  service="nova",
+  rules=["os_compute_api:os-lock-server:unlock",
+         "os_compute_api:os-lock-server:unlock:unlock_override"])
+  @decorators.idempotent_id('40dfeef9-73ee-48a9-be19-a219875de457')
+  def test_unlock_server_override(self):
+      """Test force unlock server, part of os-lock-server.
+
+      In order to trigger the unlock:unlock_override policy instead
+      of the unlock policy, the server must be locked by a different
+      user than the one who is attempting to unlock it.
+      """
+      self.os_admin.servers_client.lock_server(self.server['id'])
+      self.addCleanup(self.servers_client.unlock_server, self.server['id'])
+
+      with self.rbac_utils.override_role(self):
+          self.servers_client.unlock_server(self.server['id'])
+
+While the ``expected_error_codes`` parameter is omitted in the example above,
+Patrole automatically populates it with a 403 for each policy in ``rules``.
+Therefore, in the example above, the following expected error codes/rules
+relationship is observed:
+
+* "os_compute_api:os-lock-server:unlock" => 403
+* "os_compute_api:os-lock-server:unlock:unlock_override"  => 403
+
+Below is an example that uses ``expected_error_codes`` to account for the
+fact that Neutron is expected to raise a ``404`` on the first policy that
+is enforced server-side ("get_port"). Also, in this example, soft authorization
+is performed, meaning that it is necessary to check the response body for an
+attribute that is added only following successful policy authorization.
+
+.. code-block:: python
+
+    @utils.requires_ext(extension='binding', service='network')
+    @rbac_rule_validation.action(service="neutron",
+                                 rules=["get_port",
+                                        "get_port:binding:vif_type"],
+                                 expected_error_codes=[404, 403])
+    @decorators.idempotent_id('125aff0b-8fed-4f8e-8410-338616594b06')
+    def test_show_port_binding_vif_type(self):
+
+        # Verify specific fields of a port
+        fields = ['binding:vif_type']
+
+        with self.rbac_utils.override_role(self):
+            retrieved_port = self.ports_client.show_port(
+                self.port['id'], fields=fields)['port']
+
+        # Rather than throwing a 403, the field is not present, so raise exc.
+        if fields[0] not in retrieved_port:
+            raise rbac_exceptions.RbacMalformedResponse(
+                attribute='binding:vif_type')
+
+Note that in the example above, failure to authorize
+"get_port:binding:vif_type" results in the response body getting successfully
+returned by the server, but without additional dictionary keys. If Patrole
+fails to find those expected keys, it *acts as though* a 403 was thrown (by
+raising an exception itself, the ``rbac_rule_validation`` decorator handles
+the rest).
+
+Neutron Examples
+^^^^^^^^^^^^^^^^
+
+A basic Neutron example that only expects 403's to be raised:
+
+.. code-block:: python
+
+    @utils.requires_ext(extension='external-net', service='network')
+    @rbac_rule_validation.action(service="neutron",
+                                 rules=["create_network",
+                                        "create_network:router:external"],
+                                 expected_error_codes=[403, 403])
+    @decorators.idempotent_id('51adf2a7-739c-41e0-8857-3b4c460cbd24')
+    def test_create_network_router_external(self):
+
+        """Create External Router Network Test
+
+        RBAC test for the neutron create_network:router:external policy
+        """
+        with self.rbac_utils.override_role(self):
+            self._create_network(router_external=True)
+
+Note that above the following expected error codes/rules relationship is
+observed:
+
+* "create_network" => 403
+* "create_network:router:external"  => 403
+
+A more involved example that expects a 404 to be raised, should the first
+policy under ``rules`` fail authorization, and a 403 to be raised for any
+subsequent policy authorization failure:
+
+.. code-block:: python
+
+    @rbac_rule_validation.action(service="neutron",
+                                 rules=["get_network",
+                                        "update_network",
+                                        "update_network:shared"],
+                                 expected_error_codes=[404, 403, 403])
+    @decorators.idempotent_id('37ea3e33-47d9-49fc-9bba-1af98fbd46d6')
+    def test_update_network_shared(self):
+
+        """Update Shared Network Test
+
+        RBAC test for the neutron update_network:shared policy
+        """
+        with self.rbac_utils.override_role(self):
+            self._update_network(shared_network=True)
+        self.addCleanup(self._update_network, shared_network=False)
+
+Note that above the following expected error codes/rules relationship is
+observed:
+
+* "get_network" => 404
+* "update_network"  => 403
+* "update_network:shared" => 403
+
+Limitations
+-----------
+
+Multi-policy validation in RBAC tests comes with limitations, due to technical
+and practical challenges.
+
+Technically, there are challenges associated with multiple policies across
+cross-service API communication in OpenStack, such as between Nova and Cinder
+or Nova and Neutron. The current framework does not account for these
+cross-service policy enforcement workflows, and it is still up for debate
+whether it should.
+
+Practically, it is not possible to enumerate every policy enforced by every API
+in Patrole, as the maintenance overhead would be huge.
+
+.. _Neutron policy documentation: https://docs.openstack.org/neutron/pike/contributor/internals/policy.html
diff --git a/doc/source/rbac-overview.rst b/doc/source/rbac-overview.rst
index 09ab17d..cc47f75 100644
--- a/doc/source/rbac-overview.rst
+++ b/doc/source/rbac-overview.rst
@@ -1,3 +1,5 @@
+.. _rbac-overview:
+
 ==================================
 Role-Based Access Control Overview
 ==================================
@@ -124,12 +126,9 @@
   degree of log tracing is required by developers to confirm that the expected
   policies are getting enforced, prior to the tests getting merged.
 
-.. todo::
+For more information, see :ref:`multi-policy-validation`.
 
-  Link to multi-policy validation documentation section once it has been
-  written.
-
-.. _error-codes:
+.. _policy-error-codes:
 
 Error Codes
 -----------
@@ -196,7 +195,7 @@
     in an exception getting raised or a boolean value getting returned.
     Hard authorization results in an exception getting raised. Usually, this
     results in a ``403 Forbidden`` getting returned for unauthorized requests.
-    (See :ref:`error-codes` for further details.)
+    (See :ref:`policy-error-codes` for further details.)
 
     Related term: :term:`soft authorization`.
 
diff --git a/doc/source/test_writing_guide.rst b/doc/source/test_writing_guide.rst
index 1291201..4e0f0be 100644
--- a/doc/source/test_writing_guide.rst
+++ b/doc/source/test_writing_guide.rst
@@ -23,8 +23,8 @@
 Role overriding is the way Patrole is able to create resources and delete
 resources -- including those that require admin credentials -- while still
 being able to exercise the same set of Tempest credentials to perform the API
-action that authorizes the policy under test, by manipulating the role of
-the Tempest credentials.
+action that authorizes the policy under test, by manipulating roles of the
+Tempest credentials.
 
 Patrole implicitly splits up each test into 3 stages: set up, test execution,
 and teardown.
@@ -33,10 +33,10 @@
 
 #. Setup: Admin role is used automatically. The primary credentials are
    overridden with the admin role.
-#. Test execution: ``[patrole] rbac_test_role`` is used manually via the
+#. Test execution: ``[patrole] rbac_test_roles`` is used manually via the
    call to ``with rbac_utils.override_role(self)``. Everything that
    is executed within this contextmanager uses the primary
-   credentials overridden with the ``[patrole] rbac_test_role``.
+   credentials overridden with the ``[patrole] rbac_test_roles``.
 #. Teardown: Admin role is used automatically. The primary credentials have
    been overridden with the admin role.
 
diff --git a/etc/patrole.conf.sample b/etc/patrole.conf.sample
index 6de073d..6433f40 100644
--- a/etc/patrole.conf.sample
+++ b/etc/patrole.conf.sample
@@ -7,12 +7,17 @@
 # From patrole.config
 #
 
-# The current RBAC role against which to run Patrole
-# tests. (string value)
-#rbac_test_role = admin
+# DEPRECATED: The current RBAC role against which to run
+# Patrole tests. (string value)
+# This option is deprecated for removal.
+# Its value may be silently ignored in the future.
+# Reason: This option is deprecated and being
+# replaced with ``rbac_test_roles``.
+#rbac_test_role =
 
-# Enables RBAC tests. (boolean value)
-#enable_rbac = true
+# The current RBAC roles to be assigned to Keystone
+# Group against which to run Patrole tests. (list value)
+#rbac_test_roles = admin
 
 # List of the paths to search for policy files. Each
 # policy path assumes that the service name is included in the path
@@ -20,9 +25,11 @@
 # assumes Patrole is on the same host as the policy files. The paths
 # should be
 # ordered by precedence, with high-priority paths before low-priority
-# paths. The
-# first path that is found to contain the service's policy file will
-# be used.
+# paths. All
+# the paths that are found to contain the service's policy file will
+# be used and
+# all policy files will be merged. Allowed ``json`` or ``yaml``
+# formats.
 #  (list value)
 #custom_policy_files = /etc/%s/policy.json
 
@@ -150,3 +157,16 @@
 # This policy
 # was changed in a backwards-incompatible way. (boolean value)
 #volume_extension_volume_actions_unreserve_policy = true
+
+# 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. (boolean value)
+#removed_nova_policies_stein = true
+
+# Are the Cinder API extension policies available in the
+# cloud (e.g. [create|update|get|delete]_encryption_policy)? These
+# policies are
+# added in Stein. (boolean value)
+#added_cinder_policies_stein = true
diff --git a/patrole_tempest_plugin/config.py b/patrole_tempest_plugin/config.py
index dc0ed25..62337f7 100644
--- a/patrole_tempest_plugin/config.py
+++ b/patrole_tempest_plugin/config.py
@@ -22,8 +22,16 @@
 PatroleGroup = [
     cfg.StrOpt('rbac_test_role',
                default='admin',
+               deprecated_for_removal=True,
+               deprecated_reason="""This option is deprecated and being
+replaced with ``rbac_test_roles``.
+""",
                help="""The current RBAC role against which to run
 Patrole tests."""),
+    cfg.ListOpt('rbac_test_roles',
+                help="""List of the RBAC roles against which to run
+Patrole tests.""",
+                default=['admin']),
     cfg.ListOpt('custom_policy_files',
                 default=['/etc/%s/policy.json'],
                 help="""List of the paths to search for policy files. Each
@@ -109,7 +117,7 @@
     cfg.StrOpt('report_log_path',
                default='.',
                help="Path (relative or absolute) where the output from "
-                    "'enable_reporting' is logged. This is combined with"
+                    "'enable_reporting' is logged. This is combined with "
                     "report_log_name to generate the full path."),
 ]
 
diff --git a/patrole_tempest_plugin/hacking/checks.py b/patrole_tempest_plugin/hacking/checks.py
index 853d65c..d7b772d 100644
--- a/patrole_tempest_plugin/hacking/checks.py
+++ b/patrole_tempest_plugin/hacking/checks.py
@@ -36,7 +36,8 @@
 RULE_VALIDATION_DECORATOR = re.compile(
     r'\s*@rbac_rule_validation.action\(.*')
 IDEMPOTENT_ID_DECORATOR = re.compile(r'\s*@decorators\.idempotent_id\((.*)\)')
-PLUGIN_RBAC_TEST = re.compile(r"class .+\(.+PluginRbacTest\)")
+EXT_RBAC_TEST = re.compile(
+    r"class .+\(.+ExtRbacTest\)|class .+ExtRbacTest\(.+\)")
 
 have_rbac_decorator = False
 
@@ -212,18 +213,42 @@
             return 0, "Do not use 'self.client' as a service client alias"
 
 
-def no_plugin_rbac_test_suffix_in_plugin_test_class_name(physical_line,
-                                                         filename):
-    """Check that Plugin RBAC class names end with "PluginRbacTest"
+def no_extension_rbac_test_suffix_in_plugin_test_class_name(physical_line,
+                                                            filename):
+    """Check that Extension RBAC class names end with "ExtRbacTest"
 
     P104
     """
+    suffix = "ExtRbacTest"
     if "patrole_tempest_plugin/tests/api" in filename:
-        if PLUGIN_RBAC_TEST.match(physical_line):
-            subclass = physical_line.split('(')[0]
-            if not subclass.endswith("PluginRbacTest"):
-                error = "Plugin RBAC test classes must end in 'PluginRbacTest'"
-                return len(subclass) - 1, error
+        if EXT_RBAC_TEST.match(physical_line):
+            subclass, superclass = physical_line.split('(')
+            subclass = subclass.split('class')[1].strip()
+            superclass = superclass.split(')')[0].strip()
+            if "." in superclass:
+                superclass = superclass.split(".")[1]
+
+            both_have = all(
+                clazz.endswith(suffix) for clazz in [subclass, superclass])
+            none_have = not any(
+                clazz.endswith(suffix) for clazz in [subclass, superclass])
+
+            if not (both_have or none_have):
+                if (subclass.startswith("Base") and
+                        superclass.startswith("Base")):
+                    return
+
+                # Case 1: Subclass of "BaseExtRbacTest" must end in `suffix`
+                # Case 2: Subclass that ends in `suffix` must inherit from base
+                # class ending in `suffix`.
+                if not subclass.endswith(suffix):
+                    error = ("Plugin RBAC test subclasses must end in "
+                             "'ExtRbacTest'")
+                    return len(subclass) - 1, error
+                elif not superclass.endswith(suffix):
+                    error = ("Plugin RBAC test subclasses must inherit from a "
+                             "'ExtRbacTest' base class")
+                    return len(superclass) - 1, error
 
 
 def factory(register):
@@ -238,4 +263,4 @@
     register(no_rbac_rule_validation_decorator)
     register(no_rbac_suffix_in_test_filename)
     register(no_rbac_test_suffix_in_test_class_name)
-    register(no_plugin_rbac_test_suffix_in_plugin_test_class_name)
+    register(no_extension_rbac_test_suffix_in_plugin_test_class_name)
diff --git a/patrole_tempest_plugin/policy_authority.py b/patrole_tempest_plugin/policy_authority.py
index 2a49b6c..e0a26a3 100644
--- a/patrole_tempest_plugin/policy_authority.py
+++ b/patrole_tempest_plugin/policy_authority.py
@@ -154,17 +154,17 @@
                         if os.path.isfile(filename):
                             cls.policy_files[service].append(filename)
 
-    def allowed(self, rule_name, role):
+    def allowed(self, rule_name, roles):
         """Checks if a given rule in a policy is allowed with given role.
 
         :param string rule_name: Policy name to pass to``oslo.policy``.
-        :param string role: Role to validate for authorization.
+        :param List[string] roles: List of roles to validate for authorization.
         :raises RbacParsingException: If ``rule_name`` does not exist in the
             cloud (in policy file or among registered in-code policy defaults).
         """
-        is_admin_context = self._is_admin_context(role)
+        is_admin_context = self._is_admin_context(roles)
         is_allowed = self._allowed(
-            access=self._get_access_token(role),
+            access=self._get_access_token(roles),
             apply_rule=rule_name,
             is_admin=is_admin_context)
         return is_allowed
@@ -224,7 +224,7 @@
 
         return rules
 
-    def _is_admin_context(self, role):
+    def _is_admin_context(self, roles):
         """Checks whether a role has admin context.
 
         If context_is_admin is contained in the policy file, then checks
@@ -233,17 +233,17 @@
         """
         if 'context_is_admin' in self.rules.keys():
             return self._allowed(
-                access=self._get_access_token(role),
+                access=self._get_access_token(roles),
                 apply_rule='context_is_admin')
-        return role == CONF.identity.admin_role
+        return CONF.identity.admin_role in roles
 
-    def _get_access_token(self, role):
+    def _get_access_token(self, roles):
         access_token = {
             "token": {
                 "roles": [
                     {
                         "name": role
-                    }
+                    } for role in roles
                 ],
                 "project_id": self.project_id,
                 "tenant_id": self.project_id,
diff --git a/patrole_tempest_plugin/rbac_exceptions.py b/patrole_tempest_plugin/rbac_exceptions.py
index 6bdd7df..ad697b0 100644
--- a/patrole_tempest_plugin/rbac_exceptions.py
+++ b/patrole_tempest_plugin/rbac_exceptions.py
@@ -20,16 +20,34 @@
     message = "An unknown RBAC exception occurred"
 
 
-class RbacMalformedResponse(BasePatroleException):
-    message = ("The response body is missing the expected %(attribute)s due "
-               "to policy enforcement failure.")
+class BasePatroleResponseBodyException(BasePatroleException):
+    message = "Response body incomplete due to RBAC authorization failure"
 
-    def __init__(self, empty=False, **kwargs):
-        if empty:
-            self.message = ("The response body is empty due to policy "
-                            "enforcement failure.")
-            kwargs = {}
-        super(RbacMalformedResponse, self).__init__(**kwargs)
+
+class RbacMissingAttributeResponseBody(BasePatroleResponseBodyException):
+    """Raised when a list or show action is missing an attribute following
+    RBAC authorization failure.
+    """
+    message = ("The response body is missing the expected %(attribute)s due "
+               "to policy enforcement failure")
+
+
+class RbacPartialResponseBody(BasePatroleResponseBodyException):
+    """Raised when a list action only returns a subset of the available
+    resources.
+
+    For example, admin can return more resources than member for a list action.
+    """
+    message = ("The response body only lists a subset of the available "
+               "resources due to partial policy enforcement failure. Response "
+               "body: %(body)s")
+
+
+class RbacEmptyResponseBody(BasePatroleResponseBodyException):
+    """Raised when a list or show action is empty following RBAC authorization
+    failure.
+    """
+    message = ("The response body is empty due to policy enforcement failure.")
 
 
 class RbacResourceSetupFailed(BasePatroleException):
diff --git a/patrole_tempest_plugin/rbac_rule_validation.py b/patrole_tempest_plugin/rbac_rule_validation.py
index c85376f..9ca437b 100644
--- a/patrole_tempest_plugin/rbac_rule_validation.py
+++ b/patrole_tempest_plugin/rbac_rule_validation.py
@@ -17,6 +17,7 @@
 import logging
 import sys
 
+from oslo_log import versionutils
 from oslo_utils import excutils
 import six
 
@@ -47,7 +48,7 @@
 
     * an OpenStack service,
     * a policy action (``rule``) enforced by that service, and
-    * the test role defined by ``[patrole] rbac_test_role``
+    * the test roles defined by ``[patrole] rbac_test_roles``
 
     determines whether the test role has sufficient permissions to perform an
     API call that enforces the ``rule``.
@@ -142,7 +143,15 @@
                                                         expected_error_codes)
 
     def decorator(test_func):
-        role = CONF.patrole.rbac_test_role
+        roles = CONF.patrole.rbac_test_roles
+        # TODO(vegasq) drop once CONF.patrole.rbac_test_role is removed
+        if CONF.patrole.rbac_test_role:
+            msg = ('CONF.patrole.rbac_test_role is deprecated in favor of '
+                   'CONF.patrole.rbac_test_roles and will be removed in '
+                   'future.')
+            versionutils.report_deprecated_feature(LOG, msg)
+            if not roles:
+                roles.append(CONF.patrole.rbac_test_role)
 
         @functools.wraps(test_func)
         def wrapper(*args, **kwargs):
@@ -189,7 +198,8 @@
                     test_status = ('Error, %s' % (msg))
                     LOG.error(msg)
             except (expected_exception,
-                    rbac_exceptions.RbacMalformedResponse) as actual_exception:
+                    rbac_exceptions.BasePatroleResponseBodyException) \
+                    as actual_exception:
                 caught_exception = actual_exception
                 test_status = 'Denied'
 
@@ -200,10 +210,10 @@
                                 service)
 
                 if allowed:
-                    msg = ("Role %s was not allowed to perform the following "
-                           "actions: %s. Expected allowed actions: %s. "
-                           "Expected disallowed actions: %s." % (
-                               role, sorted(rules),
+                    msg = ("User with roles %s was not allowed to perform the "
+                           "following actions: %s. Expected allowed actions: "
+                           "%s. Expected disallowed actions: %s." % (
+                               roles, sorted(rules),
                                sorted(set(rules) - set(disallowed_rules)),
                                sorted(disallowed_rules)))
                     LOG.error(msg)
@@ -236,7 +246,7 @@
                     msg = (
                         "OverPermission: Role %s was allowed to perform the "
                         "following disallowed actions: %s" % (
-                            role, sorted(disallowed_rules)
+                            roles, sorted(disallowed_rules)
                         )
                     )
                     LOG.error(msg)
@@ -328,7 +338,12 @@
         LOG.error(msg)
         raise rbac_exceptions.RbacResourceSetupFailed(msg)
 
-    role = CONF.patrole.rbac_test_role
+    roles = CONF.patrole.rbac_test_roles
+    # TODO(vegasq) drop once CONF.patrole.rbac_test_role is removed
+    if CONF.patrole.rbac_test_role:
+        if not roles:
+            roles.append(CONF.patrole.rbac_test_role)
+
     # Test RBAC against custom requirements. Otherwise use oslo.policy.
     if CONF.patrole.test_custom_requirements:
         authority = requirements_authority.RequirementsAuthority(
@@ -339,14 +354,14 @@
         authority = policy_authority.PolicyAuthority(
             project_id, user_id, service,
             extra_target_data=formatted_target_data)
-    is_allowed = authority.allowed(rule, role)
+    is_allowed = authority.allowed(rule, roles)
 
     if is_allowed:
         LOG.debug("[Policy action]: %s, [Role]: %s is allowed!", rule,
-                  role)
+                  roles)
     else:
         LOG.debug("[Policy action]: %s, [Role]: %s is NOT allowed!",
-                  rule, role)
+                  rule, roles)
 
     return is_allowed
 
diff --git a/patrole_tempest_plugin/rbac_utils.py b/patrole_tempest_plugin/rbac_utils.py
index b7ac8d9..33955c3 100644
--- a/patrole_tempest_plugin/rbac_utils.py
+++ b/patrole_tempest_plugin/rbac_utils.py
@@ -40,7 +40,7 @@
     up, and primary credentials, needed to perform the API call which does
     policy enforcement. The primary credentials always cycle between roles
     defined by ``CONF.identity.admin_role`` and
-    ``CONF.patrole.rbac_test_role``.
+    ``CONF.patrole.rbac_test_roles``.
     """
 
     def __init__(self, test_obj):
@@ -58,10 +58,15 @@
                 "Patrole role overriding only supports v3 identity API.")
 
         self.admin_roles_client = admin_roles_client
+
+        self.user_id = test_obj.os_primary.credentials.user_id
+        self.project_id = test_obj.os_primary.credentials.tenant_id
+
+        # Change default role to admin
         self._override_role(test_obj, False)
 
     admin_role_id = None
-    rbac_role_id = None
+    rbac_role_ids = None
 
     @contextmanager
     def override_role(self, test_obj):
@@ -69,7 +74,7 @@
 
         Temporarily change the role used by ``os_primary`` credentials to:
 
-        * ``[patrole] rbac_test_role`` before test execution
+        * ``[patrole] rbac_test_roles`` before test execution
         * ``[identity] admin_role`` after test execution
 
         Automatically switches to admin role after test execution.
@@ -122,25 +127,21 @@
             * If True: role is set to ``[patrole] rbac_test_role``
             * If False: role is set to ``[identity] admin_role``
         """
-        self.user_id = test_obj.os_primary.credentials.user_id
-        self.project_id = test_obj.os_primary.credentials.tenant_id
-        self.token = test_obj.os_primary.auth_provider.get_token()
-
         LOG.debug('Overriding role to: %s.', toggle_rbac_role)
-        role_already_present = False
+        roles_already_present = False
 
         try:
-            if not all([self.admin_role_id, self.rbac_role_id]):
+            if not all([self.admin_role_id, self.rbac_role_ids]):
                 self._get_roles_by_name()
 
-            target_role = (
-                self.rbac_role_id if toggle_rbac_role else self.admin_role_id)
-            role_already_present = self._list_and_clear_user_roles_on_project(
-                target_role)
+            target_roles = (self.rbac_role_ids
+                            if toggle_rbac_role else [self.admin_role_id])
+            roles_already_present = self._list_and_clear_user_roles_on_project(
+                target_roles)
 
             # Do not override roles if `target_role` already exists.
-            if not role_already_present:
-                self._create_user_role_on_project(target_role)
+            if not roles_already_present:
+                self._create_user_role_on_project(target_roles)
         except Exception as exp:
             with excutils.save_and_reraise_exception():
                 LOG.exception(exp)
@@ -152,8 +153,8 @@
             # passing the second boundary before attempting to authenticate.
             # Only sleep if a token revocation occurred as a result of role
             # overriding. This will optimize test runtime in the case where
-            # ``[identity] admin_role`` == ``[patrole] rbac_test_role``.
-            if not role_already_present:
+            # ``[identity] admin_role`` == ``[patrole] rbac_test_roles``.
+            if not roles_already_present:
                 time.sleep(1)
 
             for provider in auth_providers:
@@ -164,41 +165,53 @@
         role_map = {r['name']: r['id'] for r in available_roles}
         LOG.debug('Available roles: %s', list(role_map.keys()))
 
-        admin_role_id = role_map.get(CONF.identity.admin_role)
-        rbac_role_id = role_map.get(CONF.patrole.rbac_test_role)
+        rbac_role_ids = []
+        roles = CONF.patrole.rbac_test_roles
+        # TODO(vegasq) drop once CONF.patrole.rbac_test_role is removed
+        if CONF.patrole.rbac_test_role:
+            if not roles:
+                roles.append(CONF.patrole.rbac_test_role)
 
-        if not all([admin_role_id, rbac_role_id]):
+        for role_name in roles:
+            rbac_role_ids.append(role_map.get(role_name))
+
+        admin_role_id = role_map.get(CONF.identity.admin_role)
+
+        if not all([admin_role_id, all(rbac_role_ids)]):
             missing_roles = []
-            msg = ("Could not find `[patrole] rbac_test_role` or "
+            msg = ("Could not find `[patrole] rbac_test_roles` or "
                    "`[identity] admin_role`, both of which are required for "
                    "RBAC testing.")
             if not admin_role_id:
                 missing_roles.append(CONF.identity.admin_role)
-            if not rbac_role_id:
-                missing_roles.append(CONF.patrole.rbac_test_role)
+            if not all(rbac_role_ids):
+                missing_roles += [role_name for role_name in roles
+                                  if not role_map.get(role_name)]
+
             msg += " Following roles were not found: %s." % (
                 ", ".join(missing_roles))
             msg += " Available roles: %s." % ", ".join(list(role_map.keys()))
             raise rbac_exceptions.RbacResourceSetupFailed(msg)
 
         self.admin_role_id = admin_role_id
-        self.rbac_role_id = rbac_role_id
+        self.rbac_role_ids = rbac_role_ids
 
-    def _create_user_role_on_project(self, role_id):
-        self.admin_roles_client.create_user_role_on_project(
-            self.project_id, self.user_id, role_id)
+    def _create_user_role_on_project(self, role_ids):
+        for role_id in role_ids:
+            self.admin_roles_client.create_user_role_on_project(
+                self.project_id, self.user_id, role_id)
 
-    def _list_and_clear_user_roles_on_project(self, role_id):
+    def _list_and_clear_user_roles_on_project(self, role_ids):
         roles = self.admin_roles_client.list_user_roles_on_project(
             self.project_id, self.user_id)['roles']
-        role_ids = [role['id'] for role in roles]
+        all_role_ids = [role['id'] for role in roles]
 
-        # NOTE(felipemonteiro): We do not use ``role_id in role_ids`` here to
-        # avoid over-permission errors: if the current list of roles on the
+        # NOTE(felipemonteiro): We do not use ``role_id in all_role_ids`` here
+        # to avoid over-permission errors: if the current list of roles on the
         # project includes "admin" and "Member", and we are switching to the
         # "Member" role, then we must delete the "admin" role. Thus, we only
         # return early if the user's roles on the project are an exact match.
-        if [role_id] == role_ids:
+        if set(role_ids) == set(all_role_ids):
             return True
 
         for role in roles:
@@ -279,8 +292,14 @@
 def is_admin():
     """Verifies whether the current test role equals the admin role.
 
-    :returns: True if ``rbac_test_role`` is the admin role.
+    :returns: True if ``rbac_test_roles`` contain the admin role.
     """
+    roles = CONF.patrole.rbac_test_roles
+    # TODO(vegasq) drop once CONF.patrole.rbac_test_role is removed
+    if CONF.patrole.rbac_test_role:
+        roles.append(CONF.patrole.rbac_test_role)
+        roles = list(set(roles))
+
     # TODO(felipemonteiro): Make this more robust via a context is admin
     # lookup.
-    return CONF.patrole.rbac_test_role == CONF.identity.admin_role
+    return CONF.identity.admin_role in roles
diff --git a/patrole_tempest_plugin/requirements_authority.py b/patrole_tempest_plugin/requirements_authority.py
index 75df9f4..4d6f25b 100644
--- a/patrole_tempest_plugin/requirements_authority.py
+++ b/patrole_tempest_plugin/requirements_authority.py
@@ -12,14 +12,16 @@
 #    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 copy
 import yaml
 
 from oslo_log import log as logging
 
 from tempest import config
-from tempest.lib import exceptions
+from tempest.lib import exceptions as lib_exc
 
 from patrole_tempest_plugin.rbac_authority import RbacAuthority
+from patrole_tempest_plugin import rbac_exceptions
 
 CONF = config.CONF
 LOG = logging.getLogger(__name__)
@@ -50,7 +52,7 @@
             <service_foo>:
               <api_action_a>:
                 - <allowed_role_1>
-                - <allowed_role_2>
+                - <allowed_role_2>,<allowed_role_3>
                 - <allowed_role_3>
               <api_action_b>:
                 - <allowed_role_2>
@@ -67,11 +69,20 @@
         try:
             for section in RequirementsParser.Inner._rbac_map:
                 if component in section:
-                    return section[component]
+                    rules = copy.copy(section[component])
+
+                    for rule in rules:
+                        rules[rule] = [
+                            roles.split(',') for roles in rules[rule]]
+
+                        for i, role_pack in enumerate(rules[rule]):
+                            rules[rule][i] = [r.strip() for r in role_pack]
+
+                    return rules
         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
+        return {}
 
 
 class RequirementsAuthority(RbacAuthority):
@@ -88,35 +99,52 @@
             Defaults to ``[patrole].custom_requirements_file``.
         :param str component: Name of the OpenStack service to be validated.
         """
-        filepath = filepath or CONF.patrole.custom_requirements_file
-
+        self.filepath = filepath or CONF.patrole.custom_requirements_file
         if component is not None:
-            self.roles_dict = RequirementsParser(filepath).parse(component)
+            self.roles_dict = RequirementsParser(self.filepath).parse(
+                component)
         else:
             self.roles_dict = None
 
-    def allowed(self, rule_name, role):
+    def allowed(self, rule_name, roles):
         """Checks if a given rule in a policy is allowed with given role.
 
         :param string rule_name: Rule to be checked using provided requirements
             file specified by ``[patrole].custom_requirements_file``. Must be
             a key present in this file, under the appropriate component.
-        :param string role: Role to validate against custom requirements file.
+        :param List[string] roles: Roles to validate against custom
+            requirements file.
         :returns: True if ``role`` is allowed to perform ``rule_name``, else
             False.
         :rtype: bool
-        :raises KeyError: If ``rule_name`` does not exist among the keyed
-            policy names in the custom requirements file.
+        :raises RbacParsingException: If ``rule_name`` does not exist among the
+            keyed policy names in the custom requirements file.
         """
-        if self.roles_dict is None:
-            raise exceptions.InvalidConfiguration(
+        if not self.roles_dict:
+            raise lib_exc.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
+            requirement_roles = self.roles_dict[rule_name]
         except KeyError:
-            raise KeyError("'%s' API is not defined in the requirements YAML "
-                           "file" % rule_name)
+            raise rbac_exceptions.RbacParsingException(
+                "'%s' rule name is not defined in the requirements YAML file: "
+                "%s" % (rule_name, self.filepath))
+
+        for role_reqs in requirement_roles:
+            required_roles = [
+                role for role in role_reqs if not role.startswith("!")]
+            forbidden_roles = [
+                role[1:] for role in role_reqs if role.startswith("!")]
+
+            # User must have all required roles
+            required_passed = all([r in roles for r in required_roles])
+            # User must not have any forbidden roles
+            forbidden_passed = all([r not in forbidden_roles
+                                    for r in roles])
+
+            if required_passed and forbidden_passed:
+                return True
+
         return False
diff --git a/patrole_tempest_plugin/tests/api/compute/test_flavor_access_rbac.py b/patrole_tempest_plugin/tests/api/compute/test_flavor_access_rbac.py
index 317c1ad..8d4d70f 100644
--- a/patrole_tempest_plugin/tests/api/compute/test_flavor_access_rbac.py
+++ b/patrole_tempest_plugin/tests/api/compute/test_flavor_access_rbac.py
@@ -50,7 +50,7 @@
 
         expected_attr = 'os-flavor-access:is_public'
         if expected_attr not in body:
-            raise rbac_exceptions.RbacMalformedResponse(
+            raise rbac_exceptions.RbacMissingAttributeResponseBody(
                 attribute=expected_attr)
 
     @testtools.skipIf(CONF.policy_feature_enabled.removed_nova_policies_stein,
@@ -71,7 +71,7 @@
         # If the `expected_attr` was not found in any flavor, then policy
         # enforcement failed.
         if not public_flavors:
-            raise rbac_exceptions.RbacMalformedResponse(
+            raise rbac_exceptions.RbacMissingAttributeResponseBody(
                 attribute=expected_attr)
 
     @decorators.idempotent_id('39cb5c8f-9990-436f-9282-fc76a41d9bac')
diff --git a/patrole_tempest_plugin/tests/api/compute/test_flavor_rxtx_rbac.py b/patrole_tempest_plugin/tests/api/compute/test_flavor_rxtx_rbac.py
index 0748e67..cbb2e19 100644
--- a/patrole_tempest_plugin/tests/api/compute/test_flavor_rxtx_rbac.py
+++ b/patrole_tempest_plugin/tests/api/compute/test_flavor_rxtx_rbac.py
@@ -45,7 +45,7 @@
         with self.rbac_utils.override_role(self):
             result = self.flavors_client.list_flavors(detail=True)['flavors']
         if 'rxtx_factor' not in result[0]:
-            raise rbac_exceptions.RbacMalformedResponse(
+            raise rbac_exceptions.RbacMissingAttributeResponseBody(
                 attribute='rxtx_factor')
 
     @testtools.skipIf(CONF.policy_feature_enabled.removed_nova_policies_stein,
@@ -59,5 +59,5 @@
             result = self.flavors_client.show_flavor(
                 CONF.compute.flavor_ref)['flavor']
         if 'rxtx_factor' not in result:
-            raise rbac_exceptions.RbacMalformedResponse(
+            raise rbac_exceptions.RbacMissingAttributeResponseBody(
                 attribute='rxtx_factor')
diff --git a/patrole_tempest_plugin/tests/api/compute/test_images_rbac.py b/patrole_tempest_plugin/tests/api/compute/test_images_rbac.py
index f6c1b67..e16222c 100644
--- a/patrole_tempest_plugin/tests/api/compute/test_images_rbac.py
+++ b/patrole_tempest_plugin/tests/api/compute/test_images_rbac.py
@@ -294,7 +294,7 @@
 
         expected_attr = 'OS-EXT-IMG-SIZE:size'
         if expected_attr not in body:
-            raise rbac_exceptions.RbacMalformedResponse(
+            raise rbac_exceptions.RbacMissingAttributeResponseBody(
                 attribute=expected_attr)
 
     @testtools.skipIf(CONF.policy_feature_enabled.removed_nova_policies_stein,
@@ -310,5 +310,5 @@
 
         expected_attr = 'OS-EXT-IMG-SIZE:size'
         if expected_attr not in body[0]:
-            raise rbac_exceptions.RbacMalformedResponse(
+            raise rbac_exceptions.RbacMissingAttributeResponseBody(
                 attribute=expected_attr)
diff --git a/patrole_tempest_plugin/tests/api/compute/test_server_actions_rbac.py b/patrole_tempest_plugin/tests/api/compute/test_server_actions_rbac.py
index a64bd20..0ff6ebe 100644
--- a/patrole_tempest_plugin/tests/api/compute/test_server_actions_rbac.py
+++ b/patrole_tempest_plugin/tests/api/compute/test_server_actions_rbac.py
@@ -403,5 +403,5 @@
             server = self.servers_client.show_server(self.server_id)['server']
 
         if 'host_status' not in server:
-            raise rbac_exceptions.RbacMalformedResponse(
+            raise rbac_exceptions.RbacMissingAttributeResponseBody(
                 attribute='host_status')
diff --git a/patrole_tempest_plugin/tests/api/compute/test_server_misc_policy_actions_rbac.py b/patrole_tempest_plugin/tests/api/compute/test_server_misc_policy_actions_rbac.py
index 88bea25..64e1300 100644
--- a/patrole_tempest_plugin/tests/api/compute/test_server_misc_policy_actions_rbac.py
+++ b/patrole_tempest_plugin/tests/api/compute/test_server_misc_policy_actions_rbac.py
@@ -143,7 +143,7 @@
         expected_attr = 'config_drive'
         # If the first server contains "config_drive", then all the others do.
         if expected_attr not in body[0]:
-            raise rbac_exceptions.RbacMalformedResponse(
+            raise rbac_exceptions.RbacMissingAttributeResponseBody(
                 attribute=expected_attr)
 
     @testtools.skipIf(CONF.policy_feature_enabled.removed_nova_policies_stein,
@@ -159,7 +159,7 @@
             body = self.servers_client.show_server(self.server['id'])['server']
         expected_attr = 'config_drive'
         if expected_attr not in body:
-            raise rbac_exceptions.RbacMalformedResponse(
+            raise rbac_exceptions.RbacMissingAttributeResponseBody(
                 attribute=expected_attr)
 
     @utils.requires_ext(extension='os-deferred-delete', service='compute')
@@ -188,7 +188,7 @@
             body = self.servers_client.list_servers(detail=True)['servers']
         # If the first server contains `expected_attr`, then all the others do.
         if expected_attr not in body[0]:
-            raise rbac_exceptions.RbacMalformedResponse(
+            raise rbac_exceptions.RbacMissingAttributeResponseBody(
                 attribute=expected_attr)
 
     @testtools.skipIf(CONF.policy_feature_enabled.removed_nova_policies_stein,
@@ -205,7 +205,7 @@
         with self.rbac_utils.override_role(self):
             body = self.servers_client.show_server(self.server['id'])['server']
         if expected_attr not in body:
-            raise rbac_exceptions.RbacMalformedResponse(
+            raise rbac_exceptions.RbacMissingAttributeResponseBody(
                 attribute=expected_attr)
 
     @testtools.skipIf(CONF.policy_feature_enabled.removed_nova_policies_stein,
@@ -229,7 +229,7 @@
         for attr in ('host', 'instance_name'):
             whole_attr = 'OS-EXT-SRV-ATTR:%s' % attr
             if whole_attr not in body[0]:
-                raise rbac_exceptions.RbacMalformedResponse(
+                raise rbac_exceptions.RbacMissingAttributeResponseBody(
                     attribute=whole_attr)
 
     @testtools.skipIf(CONF.policy_feature_enabled.removed_nova_policies_stein,
@@ -253,7 +253,7 @@
         for attr in ('host', 'instance_name'):
             whole_attr = 'OS-EXT-SRV-ATTR:%s' % attr
             if whole_attr not in body:
-                raise rbac_exceptions.RbacMalformedResponse(
+                raise rbac_exceptions.RbacMissingAttributeResponseBody(
                     attribute=whole_attr)
 
     @testtools.skipIf(CONF.policy_feature_enabled.removed_nova_policies_stein,
@@ -272,7 +272,7 @@
                           'OS-EXT-STS:power_state')
         for attr in expected_attrs:
             if attr not in body[0]:
-                raise rbac_exceptions.RbacMalformedResponse(
+                raise rbac_exceptions.RbacMissingAttributeResponseBody(
                     attribute=attr)
 
     @testtools.skipIf(CONF.policy_feature_enabled.removed_nova_policies_stein,
@@ -291,7 +291,7 @@
                           'OS-EXT-STS:power_state')
         for attr in expected_attrs:
             if attr not in body:
-                raise rbac_exceptions.RbacMalformedResponse(
+                raise rbac_exceptions.RbacMissingAttributeResponseBody(
                     attribute=attr)
 
     @testtools.skipIf(CONF.policy_feature_enabled.removed_nova_policies_stein,
@@ -310,7 +310,7 @@
         with self.rbac_utils.override_role(self):
             body = self.servers_client.list_servers(detail=True)['servers']
         if expected_attr not in body[0]:
-            raise rbac_exceptions.RbacMalformedResponse(
+            raise rbac_exceptions.RbacMissingAttributeResponseBody(
                 attribute=expected_attr)
 
     @testtools.skipIf(CONF.policy_feature_enabled.removed_nova_policies_stein,
@@ -329,7 +329,7 @@
         with self.rbac_utils.override_role(self):
             body = self.servers_client.show_server(self.server['id'])['server']
         if expected_attr not in body:
-            raise rbac_exceptions.RbacMalformedResponse(
+            raise rbac_exceptions.RbacMissingAttributeResponseBody(
                 attribute=expected_attr)
 
     @utils.requires_ext(extension='os-instance-actions', service='compute')
@@ -360,12 +360,12 @@
                 self.server['id'], request_id)['instanceAction']
 
         if 'events' not in instance_action:
-            raise rbac_exceptions.RbacMalformedResponse(
+            raise rbac_exceptions.RbacMissingAttributeResponseBody(
                 attribute='events')
         # Microversion 2.51+ returns 'events' always, but not 'traceback'. If
         # 'traceback' is also present then policy enforcement passed.
         if 'traceback' not in instance_action['events'][0]:
-            raise rbac_exceptions.RbacMalformedResponse(
+            raise rbac_exceptions.RbacMissingAttributeResponseBody(
                 attribute='events.traceback')
 
     @testtools.skipIf(CONF.policy_feature_enabled.removed_nova_policies_stein,
@@ -379,7 +379,7 @@
             result = self.servers_client.show_server(self.server['id'])[
                 'server']
         if 'key_name' not in result:
-            raise rbac_exceptions.RbacMalformedResponse(
+            raise rbac_exceptions.RbacMissingAttributeResponseBody(
                 attribute='key_name')
 
     @testtools.skipIf(CONF.policy_feature_enabled.removed_nova_policies_stein,
@@ -392,7 +392,7 @@
         with self.rbac_utils.override_role(self):
             result = self.servers_client.list_servers(detail=True)['servers']
         if 'key_name' not in result[0]:
-            raise rbac_exceptions.RbacMalformedResponse(
+            raise rbac_exceptions.RbacMissingAttributeResponseBody(
                 attribute='key_name')
 
     @rbac_rule_validation.action(
@@ -514,7 +514,7 @@
             body = self.servers_client.show_server(self.server['id'])['server']
         for expected_attr in expected_attrs:
             if expected_attr not in body:
-                raise rbac_exceptions.RbacMalformedResponse(
+                raise rbac_exceptions.RbacMissingAttributeResponseBody(
                     attribute=expected_attr)
 
     @utils.requires_ext(extension='os-simple-tenant-usage', service='compute')
diff --git a/patrole_tempest_plugin/tests/api/network/README.rst b/patrole_tempest_plugin/tests/api/network/README.rst
index 20d6196..352af8a 100644
--- a/patrole_tempest_plugin/tests/api/network/README.rst
+++ b/patrole_tempest_plugin/tests/api/network/README.rst
@@ -10,7 +10,7 @@
 broken up into the following categories:
 
 * :ref:`neutron-rbac-tests`
-* :ref:`neutron-plugin-rbac-tests`
+* :ref:`neutron-extension-rbac-tests`
 
 .. _neutron-rbac-tests:
 
@@ -22,18 +22,16 @@
 These tests are gated in many `Zuul jobs`_ (master, n-1, n-2) against many
 roles (member, admin).
 
-.. _neutron-plugin-rbac-tests:
+.. _neutron-extension-rbac-tests:
 
-Neutron plugin tests
-^^^^^^^^^^^^^^^^^^^^
+Neutron extension tests
+^^^^^^^^^^^^^^^^^^^^^^^
 
 The Neutron RBAC plugin tests focus on testing RBAC for various Neutron
-extensions and plugins, or, stated differently:
+extensions, or, stated differently: tests that rely on
+`neutron-tempest-plugin`_.
 
-* tests that rely on `neutron-tempest-plugin`_
-* external Neutron plugins
-
-These tests inherit from the base class ``BaseNetworkPluginRbacTest``. If an
+These tests inherit from the base class ``BaseNetworkExtRbacTest``. If an
 extension or plugin is not enabled in the cloud, the corresponding tests are
 gracefully skipped.
 
diff --git a/patrole_tempest_plugin/tests/api/network/rbac_base.py b/patrole_tempest_plugin/tests/api/network/rbac_base.py
index 6102347..dc0ce7f 100644
--- a/patrole_tempest_plugin/tests/api/network/rbac_base.py
+++ b/patrole_tempest_plugin/tests/api/network/rbac_base.py
@@ -13,7 +13,10 @@
 #    License for the specific language governing permissions and limitations
 #    under the License.
 
+from oslo_serialization import jsonutils as json
+
 from tempest.api.network import base as network_base
+from tempest.lib.common.utils import test_utils
 
 from patrole_tempest_plugin import rbac_utils
 
@@ -27,7 +30,7 @@
         cls.setup_rbac_utils()
 
 
-class BaseNetworkPluginRbacTest(BaseNetworkRbacTest):
+class BaseNetworkExtRbacTest(BaseNetworkRbacTest):
     """Base class to be used with tests that require neutron-tempest-plugin.
     """
 
@@ -35,14 +38,14 @@
     def get_auth_providers(cls):
         """Register auth_provider from neutron-tempest-plugin.
         """
-        providers = super(BaseNetworkPluginRbacTest, cls).get_auth_providers()
+        providers = super(BaseNetworkExtRbacTest, cls).get_auth_providers()
         if cls.is_neutron_tempest_plugin_avaliable():
             providers.append(cls.ntp_client.auth_provider)
         return providers
 
     @classmethod
     def skip_checks(cls):
-        super(BaseNetworkPluginRbacTest, cls).skip_checks()
+        super(BaseNetworkExtRbacTest, cls).skip_checks()
 
         if not cls.is_neutron_tempest_plugin_avaliable():
             msg = ("neutron-tempest-plugin not installed.")
@@ -59,7 +62,7 @@
     @classmethod
     def get_client_manager(cls, credential_type=None, roles=None,
                            force_new=None):
-        manager = super(BaseNetworkPluginRbacTest, cls).get_client_manager(
+        manager = super(BaseNetworkExtRbacTest, cls).get_client_manager(
             credential_type=credential_type,
             roles=roles,
             force_new=force_new
@@ -72,3 +75,13 @@
             cls.ntp_client = neutron_tempest_manager.network_client
 
         return manager
+
+    @classmethod
+    def create_service_profile(cls):
+        service_profile = cls.ntp_client.create_service_profile(
+            metainfo=json.dumps({'foo': 'bar'}))
+        service_profile_id = service_profile["service_profile"]["id"]
+        cls.addClassResourceCleanup(
+            test_utils.call_and_ignore_notfound_exc,
+            cls.ntp_client.delete_service_profile, service_profile_id)
+        return service_profile_id
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
index 893942e..6cdeccd 100644
--- a/patrole_tempest_plugin/tests/api/network/test_address_scope_rbac.py
+++ b/patrole_tempest_plugin/tests/api/network/test_address_scope_rbac.py
@@ -23,18 +23,18 @@
 from patrole_tempest_plugin.tests.api.network import rbac_base as base
 
 
-class AddressScopePluginRbacTest(base.BaseNetworkPluginRbacTest):
+class AddressScopeExtRbacTest(base.BaseNetworkExtRbacTest):
 
     @classmethod
     def skip_checks(cls):
-        super(AddressScopePluginRbacTest, cls).skip_checks()
+        super(AddressScopeExtRbacTest, 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(AddressScopePluginRbacTest, cls).resource_setup()
+        super(AddressScopeExtRbacTest, cls).resource_setup()
         cls.network = cls.create_network()
 
     def _create_address_scope(self, name=None, **kwargs):
diff --git a/patrole_tempest_plugin/tests/api/network/test_agents_rbac.py b/patrole_tempest_plugin/tests/api/network/test_agents_rbac.py
index c778d9c..c2b23f2 100644
--- a/patrole_tempest_plugin/tests/api/network/test_agents_rbac.py
+++ b/patrole_tempest_plugin/tests/api/network/test_agents_rbac.py
@@ -238,18 +238,18 @@
                 self.agent['id'], network_id=network_id)
 
 
-class L3AgentsPluginRbacTest(base.BaseNetworkPluginRbacTest):
+class L3AgentsExtRbacTest(base.BaseNetworkExtRbacTest):
 
     @classmethod
     def skip_checks(cls):
-        super(L3AgentsPluginRbacTest, cls).skip_checks()
+        super(L3AgentsExtRbacTest, cls).skip_checks()
         if not utils.is_extension_enabled('l3_agent_scheduler', 'network'):
             msg = "l3_agent_scheduler extension not enabled."
             raise cls.skipException(msg)
 
     @classmethod
     def resource_setup(cls):
-        super(L3AgentsPluginRbacTest, cls).resource_setup()
+        super(L3AgentsExtRbacTest, cls).resource_setup()
         name = data_utils.rand_name(cls.__name__ + '-Router')
         cls.router = cls.ntp_client.create_router(name)['router']
 
diff --git a/patrole_tempest_plugin/tests/api/network/test_auto_allocated_topology_rbac.py b/patrole_tempest_plugin/tests/api/network/test_auto_allocated_topology_rbac.py
index bcf62d7..4001255 100644
--- a/patrole_tempest_plugin/tests/api/network/test_auto_allocated_topology_rbac.py
+++ b/patrole_tempest_plugin/tests/api/network/test_auto_allocated_topology_rbac.py
@@ -20,11 +20,11 @@
 from patrole_tempest_plugin.tests.api.network import rbac_base as base
 
 
-class AutoAllocationTopologyPluginRbacTest(base.BaseNetworkPluginRbacTest):
+class AutoAllocationTopologyExtRbacTest(base.BaseNetworkExtRbacTest):
 
     @classmethod
     def skip_checks(cls):
-        super(AutoAllocationTopologyPluginRbacTest, cls).skip_checks()
+        super(AutoAllocationTopologyExtRbacTest, cls).skip_checks()
         if not utils.is_extension_enabled('auto-allocated-topology',
                                           'network'):
             msg = "auto-allocated-topology extension not enabled."
@@ -35,10 +35,44 @@
                                  rules=["get_auto_allocated_topology"],
                                  expected_error_codes=[404])
     def test_show_auto_allocated_topology(self):
-        """Show auto_allocated_topology.
+        """Test show auto_allocated_topology.
 
         RBAC test for the neutron "get_auto_allocated_topology" policy
         """
         with self.rbac_utils.override_role(self):
             self.ntp_client.get_auto_allocated_topology(
                 tenant_id=self.os_primary.credentials.tenant_id)
+
+    def _ensure_network_not_in_use(cls, network_id):
+        ports = cls.ntp_client.list_ports(network_id=network_id)["ports"]
+
+        # Every subnet within network should have a router interface
+        expected_ports_count = len(
+            cls.ntp_client.show_network(network_id)["network"]["subnets"])
+        # Every network should have a single dhcp interface
+        expected_ports_count += 1
+
+        if len(ports) != expected_ports_count:
+            msg = "Auto Allocated Topology in use."
+            cls.skipException(msg)
+
+    @decorators.idempotent_id('A0606AFE-065E-4C09-8E51-58EE7FBA30A2')
+    @decorators.attr(type='slow')
+    @rbac_rule_validation.action(service="neutron",
+                                 rules=["get_auto_allocated_topology",
+                                        "delete_auto_allocated_topology"],
+                                 expected_error_codes=[404, 403])
+    def test_delete_auto_allocated_topology(self):
+        """Test delete auto_allocated_topology.
+
+        RBAC test for the neutron "delete_auto_allocated_topology" policy
+        """
+        tenant_id = self.os_primary.credentials.tenant_id
+        net_id = self.ntp_client.get_auto_allocated_topology(
+            tenant_id=tenant_id)["auto_allocated_topology"]["id"]
+
+        self._ensure_network_not_in_use(net_id)
+
+        with self.rbac_utils.override_role(self):
+            self.ntp_client.delete_auto_allocated_topology(
+                tenant_id=self.os_primary.credentials.tenant_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
index b9f8365..e03de74 100644
--- 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
@@ -22,18 +22,18 @@
 from patrole_tempest_plugin.tests.api.network import rbac_base as base
 
 
-class DscpMarkingRulePluginRbacTest(base.BaseNetworkPluginRbacTest):
+class DscpMarkingRuleExtRbacTest(base.BaseNetworkExtRbacTest):
 
     @classmethod
     def skip_checks(cls):
-        super(DscpMarkingRulePluginRbacTest, cls).skip_checks()
+        super(DscpMarkingRuleExtRbacTest, 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()
+        super(DscpMarkingRuleExtRbacTest, 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"]
diff --git a/patrole_tempest_plugin/tests/api/network/test_flavor_service_profile_rbac.py b/patrole_tempest_plugin/tests/api/network/test_flavor_service_profile_rbac.py
new file mode 100644
index 0000000..db0b8f1
--- /dev/null
+++ b/patrole_tempest_plugin/tests/api/network/test_flavor_service_profile_rbac.py
@@ -0,0 +1,77 @@
+# 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.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 FlavorsServiceProfileExtRbacTest(base.BaseNetworkExtRbacTest):
+    @classmethod
+    def resource_setup(cls):
+        super(FlavorsServiceProfileExtRbacTest, cls).resource_setup()
+        providers = cls.ntp_client.list_service_providers()
+        if not providers["service_providers"]:
+            raise cls.skipException("No service_providers available.")
+        cls.service_type = providers["service_providers"][0]["service_type"]
+
+        cls.flavor_id = cls.create_flavor()
+        cls.service_profile_id = cls.create_service_profile()
+
+    @classmethod
+    def create_flavor(cls):
+        flavor = cls.ntp_client.create_flavor(service_type=cls.service_type)
+        flavor_id = flavor["flavor"]["id"]
+        cls.addClassResourceCleanup(
+            test_utils.call_and_ignore_notfound_exc,
+            cls.ntp_client.delete_flavor, flavor_id)
+        return flavor_id
+
+    def create_flavor_service_profile(self, flavor_id, service_profile_id):
+        self.ntp_client.create_flavor_service_profile(
+            flavor_id, service_profile_id)
+        self.addCleanup(
+            test_utils.call_and_ignore_notfound_exc,
+            self.ntp_client.delete_flavor_service_profile,
+            flavor_id, service_profile_id)
+
+    @decorators.idempotent_id('aa84b4c5-0dd6-4c34-aa81-3a76507f9b81')
+    @rbac_rule_validation.action(service="neutron",
+                                 rules=["create_flavor_service_profile"])
+    def test_create_flavor_service_profile(self):
+        """Create flavor_service_profile.
+
+        RBAC test for the neutron "create_flavor_service_profile" policy
+        """
+        with self.rbac_utils.override_role(self):
+            self.create_flavor_service_profile(self.flavor_id,
+                                               self.service_profile_id)
+
+    @decorators.idempotent_id('3b680d9e-946a-4670-ab7f-0e4576675833')
+    @rbac_rule_validation.action(service="neutron",
+                                 rules=["delete_flavor_service_profile"])
+    def test_delete_flavor_service_profile(self):
+        """Delete flavor_service_profile.
+
+        RBAC test for the neutron "delete_flavor_service_profile" policy
+        """
+        self.create_flavor_service_profile(self.flavor_id,
+                                           self.service_profile_id)
+
+        with self.rbac_utils.override_role(self):
+            self.ntp_client.delete_flavor_service_profile(
+                self.flavor_id, self.service_profile_id)
diff --git a/patrole_tempest_plugin/tests/api/network/test_flavors_rbac.py b/patrole_tempest_plugin/tests/api/network/test_flavors_rbac.py
index f8ef0bb..76c0db3 100644
--- a/patrole_tempest_plugin/tests/api/network/test_flavors_rbac.py
+++ b/patrole_tempest_plugin/tests/api/network/test_flavors_rbac.py
@@ -13,8 +13,6 @@
 #    License for the specific language governing permissions and limitations
 #    under the License.
 
-from oslo_serialization import jsonutils as json
-
 from tempest.lib.common.utils import data_utils
 from tempest.lib.common.utils import test_utils
 from tempest.lib import decorators
@@ -23,11 +21,11 @@
 from patrole_tempest_plugin.tests.api.network import rbac_base as base
 
 
-class FlavorsPluginRbacTest(base.BaseNetworkPluginRbacTest):
+class FlavorsExtRbacTest(base.BaseNetworkExtRbacTest):
 
     @classmethod
     def resource_setup(cls):
-        super(FlavorsPluginRbacTest, cls).resource_setup()
+        super(FlavorsExtRbacTest, cls).resource_setup()
         providers = cls.ntp_client.list_service_providers()
         if not providers["service_providers"]:
             raise cls.skipException("No service_providers available.")
@@ -118,70 +116,3 @@
 
         with self.rbac_utils.override_role(self):
             self.ntp_client.list_flavors()
-
-
-class FlavorsServiceProfilePluginRbacTest(base.BaseNetworkPluginRbacTest):
-    @classmethod
-    def resource_setup(cls):
-        super(FlavorsServiceProfilePluginRbacTest, cls).resource_setup()
-        providers = cls.ntp_client.list_service_providers()
-        if not providers["service_providers"]:
-            raise cls.skipException("No service_providers available.")
-        cls.service_type = providers["service_providers"][0]["service_type"]
-
-        cls.flavor_id = cls.create_flavor()
-        cls.service_profile_id = cls.create_service_profile()
-
-    @classmethod
-    def create_flavor(cls):
-        flavor = cls.ntp_client.create_flavor(service_type=cls.service_type)
-        flavor_id = flavor["flavor"]["id"]
-        cls.addClassResourceCleanup(
-            test_utils.call_and_ignore_notfound_exc,
-            cls.ntp_client.delete_flavor, flavor_id)
-        return flavor_id
-
-    @classmethod
-    def create_service_profile(cls):
-        service_profile = cls.ntp_client.create_service_profile(
-            metainfo=json.dumps({'foo': 'bar'}))
-        service_profile_id = service_profile["service_profile"]["id"]
-        cls.addClassResourceCleanup(
-            test_utils.call_and_ignore_notfound_exc,
-            cls.ntp_client.delete_service_profile, service_profile_id)
-        return service_profile_id
-
-    def create_flavor_service_profile(self, flavor_id, service_profile_id):
-        self.ntp_client.create_flavor_service_profile(
-            flavor_id, service_profile_id)
-        self.addCleanup(
-            test_utils.call_and_ignore_notfound_exc,
-            self.ntp_client.delete_flavor_service_profile,
-            flavor_id, service_profile_id)
-
-    @decorators.idempotent_id('aa84b4c5-0dd6-4c34-aa81-3a76507f9b81')
-    @rbac_rule_validation.action(service="neutron",
-                                 rules=["create_flavor_service_profile"])
-    def test_create_flavor_service_profile(self):
-        """Create flavor_service_profile.
-
-        RBAC test for the neutron "create_flavor_service_profile" policy
-        """
-        with self.rbac_utils.override_role(self):
-            self.create_flavor_service_profile(self.flavor_id,
-                                               self.service_profile_id)
-
-    @decorators.idempotent_id('3b680d9e-946a-4670-ab7f-0e4576675833')
-    @rbac_rule_validation.action(service="neutron",
-                                 rules=["delete_flavor_service_profile"])
-    def test_delete_flavor_service_profile(self):
-        """Delete flavor_service_profile.
-
-        RBAC test for the neutron "delete_flavor_service_profile" policy
-        """
-        self.create_flavor_service_profile(self.flavor_id,
-                                           self.service_profile_id)
-
-        with self.rbac_utils.override_role(self):
-            self.ntp_client.delete_flavor_service_profile(
-                self.flavor_id, self.service_profile_id)
diff --git a/patrole_tempest_plugin/tests/api/network/test_network_segments_rbac.py b/patrole_tempest_plugin/tests/api/network/test_network_segments_rbac.py
index c985111..b449970 100644
--- a/patrole_tempest_plugin/tests/api/network/test_network_segments_rbac.py
+++ b/patrole_tempest_plugin/tests/api/network/test_network_segments_rbac.py
@@ -118,4 +118,4 @@
             LOG.info("NotFound or Forbidden exception are not thrown when "
                      "role doesn't have access to the endpoint. Instead, "
                      "the response will have an empty network body.")
-            raise rbac_exceptions.RbacMalformedResponse(empty=True)
+            raise rbac_exceptions.RbacEmptyResponseBody()
diff --git a/patrole_tempest_plugin/tests/api/network/test_networks_rbac.py b/patrole_tempest_plugin/tests/api/network/test_networks_rbac.py
index 96ba378..b39489a 100644
--- a/patrole_tempest_plugin/tests/api/network/test_networks_rbac.py
+++ b/patrole_tempest_plugin/tests/api/network/test_networks_rbac.py
@@ -363,7 +363,7 @@
                 self.network['id'], **kwargs)['network']
 
         if len(retrieved_network) == 0:
-            raise rbac_exceptions.RbacMalformedResponse(empty=True)
+            raise rbac_exceptions.RbacEmptyResponseBody()
 
     @utils.requires_ext(extension='provider', service='network')
     @rbac_rule_validation.action(service="neutron",
@@ -384,7 +384,7 @@
                 self.network['id'], **kwargs)['network']
 
         if len(retrieved_network) == 0:
-            raise rbac_exceptions.RbacMalformedResponse(empty=True)
+            raise rbac_exceptions.RbacEmptyResponseBody()
 
     @utils.requires_ext(extension='provider', service='network')
     @rbac_rule_validation.action(
@@ -406,7 +406,7 @@
                 self.network['id'], **kwargs)['network']
 
         if len(retrieved_network) == 0:
-            raise rbac_exceptions.RbacMalformedResponse(empty=True)
+            raise rbac_exceptions.RbacEmptyResponseBody()
 
     @utils.requires_ext(extension='provider', service='network')
     @rbac_rule_validation.action(
@@ -428,7 +428,7 @@
                 self.network['id'], **kwargs)['network']
 
         if len(retrieved_network) == 0:
-            raise rbac_exceptions.RbacMalformedResponse(empty=True)
+            raise rbac_exceptions.RbacEmptyResponseBody()
 
     @rbac_rule_validation.action(service="neutron",
                                  rules=["get_network", "delete_network"],
diff --git a/patrole_tempest_plugin/tests/api/network/test_policy_bandwidth_limit_rule_rbac.py b/patrole_tempest_plugin/tests/api/network/test_policy_bandwidth_limit_rule_rbac.py
index 8f9635d..ab881a7 100644
--- a/patrole_tempest_plugin/tests/api/network/test_policy_bandwidth_limit_rule_rbac.py
+++ b/patrole_tempest_plugin/tests/api/network/test_policy_bandwidth_limit_rule_rbac.py
@@ -22,18 +22,18 @@
 from patrole_tempest_plugin.tests.api.network import rbac_base as base
 
 
-class PolicyBandwidthLimitRulePluginRbacTest(base.BaseNetworkPluginRbacTest):
+class PolicyBandwidthLimitRuleExtRbacTest(base.BaseNetworkExtRbacTest):
 
     @classmethod
     def skip_checks(cls):
-        super(PolicyBandwidthLimitRulePluginRbacTest, cls).skip_checks()
+        super(PolicyBandwidthLimitRuleExtRbacTest, 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(PolicyBandwidthLimitRulePluginRbacTest, cls).resource_setup()
+        super(PolicyBandwidthLimitRuleExtRbacTest, cls).resource_setup()
         name = data_utils.rand_name(cls.__class__.__name__ + '-qos-policy')
         cls.policy_id = cls.ntp_client.create_qos_policy(
             name=name)["policy"]["id"]
diff --git a/patrole_tempest_plugin/tests/api/network/test_policy_minimum_bandwidth_rule_rbac.py b/patrole_tempest_plugin/tests/api/network/test_policy_minimum_bandwidth_rule_rbac.py
index 4f85cb6..6d108af 100644
--- a/patrole_tempest_plugin/tests/api/network/test_policy_minimum_bandwidth_rule_rbac.py
+++ b/patrole_tempest_plugin/tests/api/network/test_policy_minimum_bandwidth_rule_rbac.py
@@ -22,18 +22,18 @@
 from patrole_tempest_plugin.tests.api.network import rbac_base as base
 
 
-class PolicyMinimumBandwidthRulePluginRbacTest(base.BaseNetworkPluginRbacTest):
+class PolicyMinimumBandwidthRuleExtRbacTest(base.BaseNetworkExtRbacTest):
 
     @classmethod
     def skip_checks(cls):
-        super(PolicyMinimumBandwidthRulePluginRbacTest, cls).skip_checks()
+        super(PolicyMinimumBandwidthRuleExtRbacTest, 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(PolicyMinimumBandwidthRulePluginRbacTest, cls).resource_setup()
+        super(PolicyMinimumBandwidthRuleExtRbacTest, 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"]
diff --git a/patrole_tempest_plugin/tests/api/network/test_ports_rbac.py b/patrole_tempest_plugin/tests/api/network/test_ports_rbac.py
index b65bd73..dd3537f 100644
--- a/patrole_tempest_plugin/tests/api/network/test_ports_rbac.py
+++ b/patrole_tempest_plugin/tests/api/network/test_ports_rbac.py
@@ -183,7 +183,7 @@
 
         # Rather than throwing a 403, the field is not present, so raise exc.
         if fields[0] not in retrieved_port:
-            raise rbac_exceptions.RbacMalformedResponse(
+            raise rbac_exceptions.RbacMissingAttributeResponseBody(
                 attribute='binding:vif_type')
 
     @utils.requires_ext(extension='binding', service='network')
@@ -203,7 +203,7 @@
 
         # Rather than throwing a 403, the field is not present, so raise exc.
         if fields[0] not in retrieved_port:
-            raise rbac_exceptions.RbacMalformedResponse(
+            raise rbac_exceptions.RbacMissingAttributeResponseBody(
                 attribute='binding:vif_details')
 
     @utils.requires_ext(extension='binding', service='network')
@@ -226,7 +226,7 @@
 
         # Rather than throwing a 403, the field is not present, so raise exc.
         if fields[0] not in retrieved_port:
-            raise rbac_exceptions.RbacMalformedResponse(
+            raise rbac_exceptions.RbacMissingAttributeResponseBody(
                 attribute='binding:host_id')
 
     @utils.requires_ext(extension='binding', service='network')
@@ -250,7 +250,7 @@
 
         # Rather than throwing a 403, the field is not present, so raise exc.
         if fields[0] not in retrieved_port:
-            raise rbac_exceptions.RbacMalformedResponse(
+            raise rbac_exceptions.RbacMissingAttributeResponseBody(
                 attribute='binding:profile')
 
     @rbac_rule_validation.action(service="neutron",
diff --git a/patrole_tempest_plugin/tests/api/network/test_qos_rbac.py b/patrole_tempest_plugin/tests/api/network/test_qos_rbac.py
index aae326c..3fcb7e4 100644
--- a/patrole_tempest_plugin/tests/api/network/test_qos_rbac.py
+++ b/patrole_tempest_plugin/tests/api/network/test_qos_rbac.py
@@ -22,18 +22,18 @@
 from patrole_tempest_plugin.tests.api.network import rbac_base as base
 
 
-class QosPluginRbacTest(base.BaseNetworkPluginRbacTest):
+class QosExtRbacTest(base.BaseNetworkExtRbacTest):
 
     @classmethod
     def skip_checks(cls):
-        super(QosPluginRbacTest, cls).skip_checks()
+        super(QosExtRbacTest, 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(QosPluginRbacTest, cls).resource_setup()
+        super(QosExtRbacTest, cls).resource_setup()
         cls.network = cls.create_network()
 
     def create_policy(self, name=None):
diff --git a/patrole_tempest_plugin/tests/api/network/test_rbac_policies_rbac.py b/patrole_tempest_plugin/tests/api/network/test_rbac_policies_rbac.py
index 9c88bc0..2123eb3 100644
--- a/patrole_tempest_plugin/tests/api/network/test_rbac_policies_rbac.py
+++ b/patrole_tempest_plugin/tests/api/network/test_rbac_policies_rbac.py
@@ -20,11 +20,11 @@
 from patrole_tempest_plugin.tests.api.network import rbac_base as base
 
 
-class RbacPoliciesPluginRbacTest(base.BaseNetworkPluginRbacTest):
+class RbacPoliciesExtRbacTest(base.BaseNetworkExtRbacTest):
 
     @classmethod
     def resource_setup(cls):
-        super(RbacPoliciesPluginRbacTest, cls).resource_setup()
+        super(RbacPoliciesExtRbacTest, cls).resource_setup()
         cls.tenant_id = cls.os_primary.credentials.tenant_id
         cls.network_id = cls.create_network()['id']
 
diff --git a/patrole_tempest_plugin/tests/api/network/test_routers_rbac.py b/patrole_tempest_plugin/tests/api/network/test_routers_rbac.py
index f850a3e..399ad47 100644
--- a/patrole_tempest_plugin/tests/api/network/test_routers_rbac.py
+++ b/patrole_tempest_plugin/tests/api/network/test_routers_rbac.py
@@ -179,7 +179,7 @@
 
         # Rather than throwing a 403, the field is not present, so raise exc.
         if 'distributed' not in retrieved_fields:
-            raise rbac_exceptions.RbacMalformedResponse(
+            raise rbac_exceptions.RbacMissingAttributeResponseBody(
                 attribute='distributed')
 
     @decorators.idempotent_id('defc502c-4159-4824-b4d9-3cdcc39015b2')
@@ -201,7 +201,7 @@
 
         # Rather than throwing a 403, the field is not present, so raise exc.
         if 'ha' not in retrieved_fields:
-            raise rbac_exceptions.RbacMalformedResponse(
+            raise rbac_exceptions.RbacMissingAttributeResponseBody(
                 attribute='ha')
 
     @rbac_rule_validation.action(service="neutron",
diff --git a/patrole_tempest_plugin/tests/api/network/test_security_groups_rbac.py b/patrole_tempest_plugin/tests/api/network/test_security_groups_rbac.py
index 9112bf6..e9fa018 100644
--- a/patrole_tempest_plugin/tests/api/network/test_security_groups_rbac.py
+++ b/patrole_tempest_plugin/tests/api/network/test_security_groups_rbac.py
@@ -126,7 +126,7 @@
 
         # Neutron may return an empty list if access is denied.
         if not security_groups['security_groups']:
-            raise rbac_exceptions.RbacMalformedResponse(empty=True)
+            raise rbac_exceptions.RbacEmptyResponseBody()
 
     @rbac_rule_validation.action(service="neutron",
                                  rules=["create_security_group_rule"])
@@ -170,4 +170,4 @@
 
         # Neutron may return an empty list if access is denied.
         if not security_rules['security_group_rules']:
-            raise rbac_exceptions.RbacMalformedResponse(empty=True)
+            raise rbac_exceptions.RbacEmptyResponseBody()
diff --git a/patrole_tempest_plugin/tests/api/network/test_segments_rbac.py b/patrole_tempest_plugin/tests/api/network/test_segments_rbac.py
index 9725e2b..0b58649 100644
--- a/patrole_tempest_plugin/tests/api/network/test_segments_rbac.py
+++ b/patrole_tempest_plugin/tests/api/network/test_segments_rbac.py
@@ -23,18 +23,18 @@
 from patrole_tempest_plugin.tests.api.network import rbac_base as base
 
 
-class SegmentsPluginRbacTest(base.BaseNetworkPluginRbacTest):
+class SegmentsExtRbacTest(base.BaseNetworkExtRbacTest):
 
     @classmethod
     def skip_checks(cls):
-        super(SegmentsPluginRbacTest, cls).skip_checks()
+        super(SegmentsExtRbacTest, cls).skip_checks()
         if not utils.is_extension_enabled('segment', 'network'):
             msg = "segment extension not enabled."
             raise cls.skipException(msg)
 
     @classmethod
     def resource_setup(cls):
-        super(SegmentsPluginRbacTest, cls).resource_setup()
+        super(SegmentsExtRbacTest, cls).resource_setup()
         cls.network = cls.create_network()
 
     @classmethod
diff --git a/patrole_tempest_plugin/tests/api/network/test_service_profile_rbac.py b/patrole_tempest_plugin/tests/api/network/test_service_profile_rbac.py
new file mode 100644
index 0000000..9e82835
--- /dev/null
+++ b/patrole_tempest_plugin/tests/api/network/test_service_profile_rbac.py
@@ -0,0 +1,73 @@
+# 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.lib import decorators
+
+from patrole_tempest_plugin import rbac_rule_validation
+from patrole_tempest_plugin.tests.api.network import rbac_base as base
+
+
+class ServiceProfileExtRbacTest(base.BaseNetworkExtRbacTest):
+    @decorators.idempotent_id('6ce76efa-7400-44c1-80ec-58f79b1d89ca')
+    @rbac_rule_validation.action(service="neutron",
+                                 rules=["create_service_profile"])
+    def test_create_service_profile(self):
+        """Create service profile
+
+        RBAC test for the neutron "create_service_profile" policy
+        """
+        with self.rbac_utils.override_role(self):
+            self.create_service_profile()
+
+    @decorators.idempotent_id('e4c473b7-3ae9-4a2e-8cac-848f7b01187d')
+    @rbac_rule_validation.action(service="neutron",
+                                 rules=["get_service_profile"],
+                                 expected_error_codes=[404])
+    def test_show_service_profile(self):
+        """Show service profile
+
+        RBAC test for the neutron "get_service_profile" policy
+        """
+        profile_id = self.create_service_profile()
+        with self.rbac_utils.override_role(self):
+            self.ntp_client.show_service_profile(profile_id)
+
+    @decorators.idempotent_id('a3dd719d-4cd3-40cc-b4f1-5642e2717adf')
+    @rbac_rule_validation.action(service="neutron",
+                                 rules=["get_service_profile",
+                                        "update_service_profile"],
+                                 expected_error_codes=[404, 403])
+    def test_update_service_profile(self):
+        """Update service profile
+
+        RBAC test for the neutron "update_service_profile" policy
+        """
+        profile_id = self.create_service_profile()
+        with self.rbac_utils.override_role(self):
+            self.ntp_client.update_service_profile(profile_id, enabled=False)
+
+    @decorators.idempotent_id('926b60c2-04fe-4339-aa44-bf27121392e8')
+    @rbac_rule_validation.action(service="neutron",
+                                 rules=["get_service_profile",
+                                        "delete_service_profile"],
+                                 expected_error_codes=[404, 403])
+    def test_delete_service_profile(self):
+        """Delete service profile
+
+        RBAC test for the neutron "delete_service_profile" policy
+        """
+        profile_id = self.create_service_profile()
+        with self.rbac_utils.override_role(self):
+            self.ntp_client.delete_service_profile(profile_id)
diff --git a/patrole_tempest_plugin/tests/api/network/test_subnets_rbac.py b/patrole_tempest_plugin/tests/api/network/test_subnets_rbac.py
index 9a5ebe4..8fe157a 100644
--- a/patrole_tempest_plugin/tests/api/network/test_subnets_rbac.py
+++ b/patrole_tempest_plugin/tests/api/network/test_subnets_rbac.py
@@ -73,7 +73,7 @@
 
         # Neutron may return an empty list if access is denied.
         if not subnets['subnets']:
-            raise rbac_exceptions.RbacMalformedResponse(empty=True)
+            raise rbac_exceptions.RbacEmptyResponseBody()
 
     @decorators.idempotent_id('f36cd821-dd22-4bd0-b43d-110fc4b553eb')
     @rbac_rule_validation.action(service="neutron",
diff --git a/patrole_tempest_plugin/tests/api/network/test_trunks_rbac.py b/patrole_tempest_plugin/tests/api/network/test_trunks_rbac.py
index 063fd55..4b2eefd 100644
--- a/patrole_tempest_plugin/tests/api/network/test_trunks_rbac.py
+++ b/patrole_tempest_plugin/tests/api/network/test_trunks_rbac.py
@@ -21,18 +21,18 @@
 from patrole_tempest_plugin.tests.api.network import rbac_base as base
 
 
-class TrunksPluginRbacTest(base.BaseNetworkPluginRbacTest):
+class TrunksExtRbacTest(base.BaseNetworkExtRbacTest):
 
     @classmethod
     def skip_checks(cls):
-        super(TrunksPluginRbacTest, cls).skip_checks()
+        super(TrunksExtRbacTest, cls).skip_checks()
         if not utils.is_extension_enabled('trunk', 'network'):
             msg = "trunk extension not enabled."
             raise cls.skipException(msg)
 
     @classmethod
     def resource_setup(cls):
-        super(TrunksPluginRbacTest, cls).resource_setup()
+        super(TrunksExtRbacTest, cls).resource_setup()
         cls.network = cls.create_network()
         cls.port_id = cls.create_port(cls.network)["id"]
 
diff --git a/patrole_tempest_plugin/tests/api/volume/rbac_base.py b/patrole_tempest_plugin/tests/api/volume/rbac_base.py
index 14b3151..1d0a44d 100644
--- a/patrole_tempest_plugin/tests/api/volume/rbac_base.py
+++ b/patrole_tempest_plugin/tests/api/volume/rbac_base.py
@@ -32,10 +32,10 @@
     def setup_clients(cls):
         super(BaseVolumeRbacTest, cls).setup_clients()
         cls.setup_rbac_utils()
-        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
+        cls.volume_hosts_client = cls.os_primary.volume_hosts_client_latest
+        cls.volume_types_client = cls.os_primary.volume_types_client_latest
+        cls.groups_client = cls.os_primary.groups_client_latest
+        cls.group_types_client = cls.os_primary.group_types_client_latest
 
     @classmethod
     def create_volume_type(cls, name=None, **kwargs):
diff --git a/patrole_tempest_plugin/tests/api/volume/test_capabilities_rbac.py b/patrole_tempest_plugin/tests/api/volume/test_capabilities_rbac.py
index fa1157a..3770f84 100644
--- a/patrole_tempest_plugin/tests/api/volume/test_capabilities_rbac.py
+++ b/patrole_tempest_plugin/tests/api/volume/test_capabilities_rbac.py
@@ -32,8 +32,9 @@
     @classmethod
     def setup_clients(cls):
         super(CapabilitiesV3RbacTest, cls).setup_clients()
-        cls.capabilities_client = cls.os_primary.volume_capabilities_v2_client
-        cls.hosts_client = cls.os_primary.volume_hosts_v2_client
+        cls.capabilities_client = \
+            cls.os_primary.volume_capabilities_client_latest
+        cls.hosts_client = cls.os_primary.volume_hosts_client_latest
 
     @rbac_rule_validation.action(service="cinder",
                                  rules=["volume_extension:capabilities"])
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 0eb0244..8443943 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
@@ -56,7 +56,8 @@
     @classmethod
     def setup_clients(cls):
         super(EncryptionTypesV3RbacTest, cls).setup_clients()
-        cls.encryption_types_client = cls.os_primary.encryption_types_v2_client
+        cls.encryption_types_client = \
+            cls.os_primary.encryption_types_client_latest
 
     def _create_volume_type_encryption(self):
         vol_type_id = self.create_volume_type()['id']
diff --git a/patrole_tempest_plugin/tests/api/volume/test_group_snapshots_rbac.py b/patrole_tempest_plugin/tests/api/volume/test_group_snapshots_rbac.py
index 1d59f9b..56a0233 100644
--- a/patrole_tempest_plugin/tests/api/volume/test_group_snapshots_rbac.py
+++ b/patrole_tempest_plugin/tests/api/volume/test_group_snapshots_rbac.py
@@ -75,7 +75,7 @@
     def setup_clients(cls):
         super(GroupSnaphotsV314RbacTest, cls).setup_clients()
         cls.group_snapshot_client = \
-            cls.os_primary.group_snapshots_v3_client
+            cls.os_primary.group_snapshots_client_latest
 
     def setUp(self):
         super(GroupSnaphotsV314RbacTest, self).setUp()
@@ -172,7 +172,7 @@
     def setup_clients(cls):
         super(GroupSnaphotsV319RbacTest, cls).setup_clients()
         cls.group_snapshot_client = \
-            cls.os_primary.group_snapshots_v3_client
+            cls.os_primary.group_snapshots_client_latest
 
     def setUp(self):
         super(GroupSnaphotsV319RbacTest, self).setUp()
diff --git a/patrole_tempest_plugin/tests/api/volume/test_groups_rbac.py b/patrole_tempest_plugin/tests/api/volume/test_groups_rbac.py
index c117d23..730e349 100644
--- a/patrole_tempest_plugin/tests/api/volume/test_groups_rbac.py
+++ b/patrole_tempest_plugin/tests/api/volume/test_groups_rbac.py
@@ -208,7 +208,7 @@
             group_type = self.create_group_type(ignore_notfound=True)
 
         if 'group_specs' not in group_type:
-            raise rbac_exceptions.RbacMalformedResponse(
+            raise rbac_exceptions.RbacMissingAttributeResponseBody(
                 attribute='group_specs')
 
     @decorators.idempotent_id('8d9e2831-24c3-47b7-a76a-2e563287f12f')
@@ -221,5 +221,5 @@
             resp_body = self.group_types_client.show_group_type(
                 group_type['id'])['group_type']
         if 'group_specs' not in resp_body:
-            raise rbac_exceptions.RbacMalformedResponse(
+            raise rbac_exceptions.RbacMissingAttributeResponseBody(
                 attribute='group_specs')
diff --git a/patrole_tempest_plugin/tests/api/volume/test_limits_rbac.py b/patrole_tempest_plugin/tests/api/volume/test_limits_rbac.py
index 3127d83..2bd0992 100644
--- a/patrole_tempest_plugin/tests/api/volume/test_limits_rbac.py
+++ b/patrole_tempest_plugin/tests/api/volume/test_limits_rbac.py
@@ -51,4 +51,5 @@
                 'limits']['absolute']
         for key in expected_keys:
             if key not in absolute_limits:
-                raise rbac_exceptions.RbacMalformedResponse(attribute=key)
+                raise rbac_exceptions.RbacMissingAttributeResponseBody(
+                    attribute=key)
diff --git a/patrole_tempest_plugin/tests/api/volume/test_qos_rbac.py b/patrole_tempest_plugin/tests/api/volume/test_qos_rbac.py
index 5664bf9..2f144b0 100644
--- a/patrole_tempest_plugin/tests/api/volume/test_qos_rbac.py
+++ b/patrole_tempest_plugin/tests/api/volume/test_qos_rbac.py
@@ -27,7 +27,7 @@
     @classmethod
     def setup_clients(cls):
         super(VolumeQOSV3RbacTest, cls).setup_clients()
-        cls.qos_client = cls.os_primary.volume_qos_v2_client
+        cls.qos_client = cls.os_primary.volume_qos_client_latest
 
     def _create_test_qos_specs(self, name=None, consumer=None, **kwargs):
         name = name or data_utils.rand_name(self.__class__.__name__ + '-QoS')
diff --git a/patrole_tempest_plugin/tests/api/volume/test_scheduler_stats_rbac.py b/patrole_tempest_plugin/tests/api/volume/test_scheduler_stats_rbac.py
index efcfdaf..ff12cba 100644
--- a/patrole_tempest_plugin/tests/api/volume/test_scheduler_stats_rbac.py
+++ b/patrole_tempest_plugin/tests/api/volume/test_scheduler_stats_rbac.py
@@ -33,7 +33,7 @@
     def setup_clients(cls):
         super(SchedulerStatsV3RbacTest, cls).setup_clients()
         cls.scheduler_stats_client =\
-            cls.os_primary.volume_scheduler_stats_v2_client
+            cls.os_primary.volume_scheduler_stats_client_latest
 
     @rbac_rule_validation.action(
         service="cinder",
diff --git a/patrole_tempest_plugin/tests/api/volume/test_snapshot_manage_rbac.py b/patrole_tempest_plugin/tests/api/volume/test_snapshot_manage_rbac.py
index e2887e0..1fc4c24 100644
--- a/patrole_tempest_plugin/tests/api/volume/test_snapshot_manage_rbac.py
+++ b/patrole_tempest_plugin/tests/api/volume/test_snapshot_manage_rbac.py
@@ -40,7 +40,8 @@
     @classmethod
     def setup_clients(cls):
         super(SnapshotManageRbacTest, cls).setup_clients()
-        cls.snapshot_manage_client = cls.os_primary.snapshot_manage_v2_client
+        cls.snapshot_manage_client = \
+            cls.os_primary.snapshot_manage_client_latest
 
     @classmethod
     def resource_setup(cls):
diff --git a/patrole_tempest_plugin/tests/api/volume/test_user_messages_rbac.py b/patrole_tempest_plugin/tests/api/volume/test_user_messages_rbac.py
index 962a9b1..11c42b1 100644
--- a/patrole_tempest_plugin/tests/api/volume/test_user_messages_rbac.py
+++ b/patrole_tempest_plugin/tests/api/volume/test_user_messages_rbac.py
@@ -31,7 +31,7 @@
     @classmethod
     def setup_clients(cls):
         super(MessagesV3RbacTest, cls).setup_clients()
-        cls.messages_client = cls.os_primary.volume_v3_messages_client
+        cls.messages_client = cls.os_primary.volume_messages_client_latest
 
     def _create_user_message(self):
         """Trigger a 'no valid host' situation to generate a message."""
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 6c2c84d..7e0044d 100644
--- a/patrole_tempest_plugin/tests/api/volume/test_volume_metadata_rbac.py
+++ b/patrole_tempest_plugin/tests/api/volume/test_volume_metadata_rbac.py
@@ -111,7 +111,7 @@
                 'volumes']
         expected_attr = 'volume_image_metadata'
         if expected_attr not in resp_body[0]:
-            raise rbac_exceptions.RbacMalformedResponse(
+            raise rbac_exceptions.RbacMissingAttributeResponseBody(
                 attribute=expected_attr)
 
     @decorators.idempotent_id('53f94d52-0dd5-42cf-a3a4-59b35150b3d5')
@@ -129,7 +129,7 @@
                 'volume']
         expected_attr = 'volume_image_metadata'
         if expected_attr not in resp_body:
-            raise rbac_exceptions.RbacMalformedResponse(
+            raise rbac_exceptions.RbacMissingAttributeResponseBody(
                 attribute=expected_attr)
 
     @decorators.idempotent_id('a9d9e825-5ea3-42e6-96f3-7ac4e97b2ed0')
diff --git a/patrole_tempest_plugin/tests/api/volume/test_volume_quotas_rbac.py b/patrole_tempest_plugin/tests/api/volume/test_volume_quotas_rbac.py
index cd1fb6e..f49c2fd 100644
--- a/patrole_tempest_plugin/tests/api/volume/test_volume_quotas_rbac.py
+++ b/patrole_tempest_plugin/tests/api/volume/test_volume_quotas_rbac.py
@@ -32,7 +32,7 @@
     @classmethod
     def setup_clients(cls):
         super(VolumeQuotasV3RbacTest, cls).setup_clients()
-        cls.quotas_client = cls.os_primary.volume_quotas_v2_client
+        cls.quotas_client = cls.os_primary.volume_quotas_client_latest
 
     def _restore_default_quota_set(self):
         default_quota_set = self.quotas_client.show_default_quota_set(
diff --git a/patrole_tempest_plugin/tests/api/volume/test_volume_services_rbac.py b/patrole_tempest_plugin/tests/api/volume/test_volume_services_rbac.py
index 9f97a82..0f4e458 100644
--- a/patrole_tempest_plugin/tests/api/volume/test_volume_services_rbac.py
+++ b/patrole_tempest_plugin/tests/api/volume/test_volume_services_rbac.py
@@ -36,7 +36,7 @@
     @classmethod
     def setup_clients(cls):
         super(VolumeServicesV3RbacTest, cls).setup_clients()
-        cls.services_client = cls.os_primary.volume_services_v2_client
+        cls.services_client = cls.os_primary.volume_services_client_latest
 
     @decorators.idempotent_id('b9134f01-97c0-4abd-9455-fe2f03e3f966')
     @rbac_rule_validation.action(
diff --git a/patrole_tempest_plugin/tests/api/volume/test_volume_transfers_rbac.py b/patrole_tempest_plugin/tests/api/volume/test_volume_transfers_rbac.py
index a18a370..5e0fd21 100644
--- a/patrole_tempest_plugin/tests/api/volume/test_volume_transfers_rbac.py
+++ b/patrole_tempest_plugin/tests/api/volume/test_volume_transfers_rbac.py
@@ -26,7 +26,7 @@
     @classmethod
     def setup_clients(cls):
         super(VolumesTransfersV3RbacTest, cls).setup_clients()
-        cls.transfers_client = cls.os_primary.volume_transfers_v2_client
+        cls.transfers_client = cls.os_primary.volume_transfers_client_latest
 
     @classmethod
     def resource_setup(cls):
@@ -54,12 +54,17 @@
     def test_create_volume_transfer(self):
         with self.rbac_utils.override_role(self):
             self._create_transfer()
+        waiters.wait_for_volume_resource_status(
+            self.volumes_client, self.volume['id'], 'awaiting-transfer')
 
     @rbac_rule_validation.action(service="cinder",
                                  rules=["volume:get_transfer"])
     @decorators.idempotent_id('7a0925d3-ed97-4c25-8299-e5cdabe2eb55')
     def test_get_volume_transfer(self):
         transfer = self._create_transfer()
+        waiters.wait_for_volume_resource_status(
+            self.volumes_client, self.volume['id'], 'awaiting-transfer')
+
         with self.rbac_utils.override_role(self):
             self.transfers_client.show_volume_transfer(transfer['id'])
 
@@ -82,15 +87,23 @@
     @decorators.idempotent_id('987f2a11-d657-4984-a6c9-28f06c1cd014')
     def test_accept_volume_transfer(self):
         transfer = self._create_transfer()
+        waiters.wait_for_volume_resource_status(
+            self.volumes_client, self.volume['id'], 'awaiting-transfer')
+
         with self.rbac_utils.override_role(self):
             self.transfers_client.accept_volume_transfer(
                 transfer['id'], auth_key=transfer['auth_key'])
+        waiters.wait_for_volume_resource_status(self.volumes_client,
+                                                self.volume['id'], 'available')
 
     @rbac_rule_validation.action(service="cinder",
                                  rules=["volume:delete_transfer"])
     @decorators.idempotent_id('4672187e-7fff-454b-832a-5c8865dda868')
     def test_delete_volume_transfer(self):
         transfer = self._create_transfer()
+        waiters.wait_for_volume_resource_status(
+            self.volumes_client, self.volume['id'], 'awaiting-transfer')
+
         with self.rbac_utils.override_role(self):
             self.transfers_client.delete_volume_transfer(transfer['id'])
         waiters.wait_for_volume_resource_status(
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 bf22341..0efeb33 100644
--- a/patrole_tempest_plugin/tests/api/volume/test_volumes_backup_rbac.py
+++ b/patrole_tempest_plugin/tests/api/volume/test_volumes_backup_rbac.py
@@ -210,7 +210,7 @@
         # Show backup API attempts to inject the attribute below into the
         # response body but only if policy enforcement succeeds.
         if self.expected_attr not in body:
-            raise rbac_exceptions.RbacMalformedResponse(
+            raise rbac_exceptions.RbacMissingAttributeResponseBody(
                 attribute=self.expected_attr)
 
     @decorators.idempotent_id('aa40b7c0-5974-48be-8cbc-e23cc61c4c68')
@@ -221,7 +221,7 @@
             body = self.backups_client.list_backups(detail=True)['backups']
 
         if self.expected_attr not in body[0]:
-            raise rbac_exceptions.RbacMalformedResponse(
+            raise rbac_exceptions.RbacMissingAttributeResponseBody(
                 attribute=self.expected_attr)
 
 
diff --git a/patrole_tempest_plugin/tests/api/volume/test_volumes_manage_rbac.py b/patrole_tempest_plugin/tests/api/volume/test_volumes_manage_rbac.py
index 2782e22..9f21c4a 100644
--- a/patrole_tempest_plugin/tests/api/volume/test_volumes_manage_rbac.py
+++ b/patrole_tempest_plugin/tests/api/volume/test_volumes_manage_rbac.py
@@ -41,7 +41,7 @@
     @classmethod
     def setup_clients(cls):
         super(VolumesManageV3RbacTest, cls).setup_clients()
-        cls.volume_manage_client = cls.os_primary.volume_manage_v2_client
+        cls.volume_manage_client = cls.os_primary.volume_manage_client_latest
 
     def _manage_volume(self, org_volume):
         # Manage volume
diff --git a/patrole_tempest_plugin/tests/api/volume/test_volumes_snapshots_rbac.py b/patrole_tempest_plugin/tests/api/volume/test_volumes_snapshots_rbac.py
index 40469a2..55adf1a 100644
--- a/patrole_tempest_plugin/tests/api/volume/test_volumes_snapshots_rbac.py
+++ b/patrole_tempest_plugin/tests/api/volume/test_volumes_snapshots_rbac.py
@@ -76,7 +76,7 @@
                 self.snapshot['id'])['snapshot']
         for expected_attr in expected_attrs:
             if expected_attr not in resp:
-                raise rbac_exceptions.RbacMalformedResponse(
+                raise rbac_exceptions.RbacMissingAttributeResponseBody(
                     attribute=expected_attr)
 
     @rbac_rule_validation.action(service="cinder",
@@ -136,5 +136,5 @@
             resp = self._list_by_param_values(with_detail=True, **params)
         for expected_attr in expected_attrs:
             if expected_attr not in resp[0]:
-                raise rbac_exceptions.RbacMalformedResponse(
+                raise rbac_exceptions.RbacMissingAttributeResponseBody(
                     attribute=expected_attr)
diff --git a/patrole_tempest_plugin/tests/unit/fixtures.py b/patrole_tempest_plugin/tests/unit/fixtures.py
index aee36fa..78e87e5 100644
--- a/patrole_tempest_plugin/tests/unit/fixtures.py
+++ b/patrole_tempest_plugin/tests/unit/fixtures.py
@@ -66,7 +66,8 @@
     def setUp(self):
         super(RbacUtilsFixture, self).setUp()
 
-        self.useFixture(ConfPatcher(rbac_test_role='member', group='patrole'))
+        self.useFixture(ConfPatcher(rbac_test_roles=['member'],
+                                    group='patrole'))
         self.useFixture(ConfPatcher(
             admin_role='admin', auth_version='v3', group='identity'))
         self.useFixture(ConfPatcher(
@@ -92,7 +93,7 @@
         mock_admin_mgr = mock.patch.object(
             clients, 'Manager', spec=clients.Manager,
             roles_v3_client=mock.Mock(), roles_client=mock.Mock()).start()
-        self.roles_v3_client = mock_admin_mgr.return_value.roles_v3_client
+        self.admin_roles_client = mock_admin_mgr.return_value.roles_v3_client
 
         self.set_roles(['admin', 'member'], [])
 
@@ -153,6 +154,6 @@
                       for role in roles_on_project]
         }
 
-        self.roles_v3_client.list_roles.return_value = available_roles
-        self.roles_v3_client.list_user_roles_on_project.return_value = (
+        self.admin_roles_client.list_roles.return_value = available_roles
+        self.admin_roles_client.list_user_roles_on_project.return_value = (
             available_project_roles)
diff --git a/patrole_tempest_plugin/tests/unit/resources/rbac_roles.yaml b/patrole_tempest_plugin/tests/unit/resources/rbac_roles.yaml
index c5436d0..357e0e6 100644
--- a/patrole_tempest_plugin/tests/unit/resources/rbac_roles.yaml
+++ b/patrole_tempest_plugin/tests/unit/resources/rbac_roles.yaml
@@ -4,3 +4,7 @@
     - _member_
   test:create2:
     - test_member
+  test:create3:
+    - test_member, _member_
+  test:create4:
+    - test_member, !_member_
\ No newline at end of file
diff --git a/patrole_tempest_plugin/tests/unit/test_hacking.py b/patrole_tempest_plugin/tests/unit/test_hacking.py
index f75bffe..a0ace76 100644
--- a/patrole_tempest_plugin/tests/unit/test_hacking.py
+++ b/patrole_tempest_plugin/tests/unit/test_hacking.py
@@ -257,12 +257,12 @@
             "  cls.client",
             "./patrole_tempest_plugin/tests/api/fake_test_rbac.py"))
 
-    def test_no_plugin_rbac_test_suffix_in_plugin_test_class_name(self):
-        check = checks.no_plugin_rbac_test_suffix_in_plugin_test_class_name
+    def no_extension_rbac_test_suffix_in_plugin_test_class_name(self):
+        check = checks.no_extension_rbac_test_suffix_in_plugin_test_class_name
 
-        # Passing cases: these do not inherit from "PluginRbacTest" base class.
+        # Passing cases: these do not inherit from "ExtRbacTest" base class.
         self.assertFalse(check(
-            "class FakeRbacTest",
+            "class FakeRbacTest(BaseFakeRbacTest)",
             "./patrole_tempest_plugin/tests/api/fake_test_rbac.py"))
         self.assertFalse(check(
             "class FakeRbacTest(base.BaseFakeRbacTest)",
@@ -270,16 +270,39 @@
 
         # Passing cases: these **do** end in correct test class suffix.
         self.assertFalse(check(
-            "class FakePluginRbacTest",
+            "class FakeExtRbacTest(BaseFakeExtRbacTest)",
             "./patrole_tempest_plugin/tests/api/fake_test_rbac.py"))
         self.assertFalse(check(
-            "class FakePluginRbacTest(base.BaseFakeRbacTest)",
+            "class FakeExtRbacTest(base.BaseFakeExtRbacTest)",
+            "./patrole_tempest_plugin/tests/api/fake_test_rbac.py"))
+
+        # Passing cases: plugin base class inherits from another base class.
+        self.assertFalse(check(
+            "class BaseFakeExtRbacTest(base.BaseFakeRbacTest)",
+            "./patrole_tempest_plugin/tests/api/fake_test_rbac.py"))
+        self.assertFalse(check(
+            "class BaseFakeExtRbacTest(BaseFakeRbacTest)",
             "./patrole_tempest_plugin/tests/api/fake_test_rbac.py"))
 
         # Failing cases: these **do not** end in correct test class suffix.
+        # Case 1: RbacTest subclass doesn't end in ExtRbacTest.
         self.assertTrue(check(
-            "class FakeRbacTest(BaseFakePluginRbacTest)",
+            "class FakeRbacTest(base.BaseFakeExtRbacTest)",
             "./patrole_tempest_plugin/tests/api/fake_test_rbac.py"))
         self.assertTrue(check(
-            "class FakeRbacTest(BaseFakeNetworkPluginRbacTest)",
+            "class FakeRbacTest(BaseFakeExtRbacTest)",
+            "./patrole_tempest_plugin/tests/api/fake_test_rbac.py"))
+        self.assertTrue(check(
+            "class FakeRbacTest(BaseFakeNetworkExtRbacTest)",
             "./patrole_tempest_plugin/tests/api/network/fake_test_rbac.py"))
+        # Case 2: ExtRbacTest subclass doesn't inherit from
+        # BaseExtRbacTest.
+        self.assertTrue(check(
+            "class FakeExtRbacTest(base.BaseFakeRbacTest)",
+            "./patrole_tempest_plugin/tests/api/fake_test_rbac.py"))
+        self.assertTrue(check(
+            "class FakeExtRbacTest(BaseFakeRbacTest)",
+            "./patrole_tempest_plugin/tests/api/fake_test_rbac.py"))
+        self.assertTrue(check(
+            "class FakeNeutronExtRbacTest(BaseFakeNeutronRbacTest)",
+            "./patrole_tempest_plugin/tests/api/fake_test_rbac.py"))
diff --git a/patrole_tempest_plugin/tests/unit/test_policy_authority.py b/patrole_tempest_plugin/tests/unit/test_policy_authority.py
index 624c0c5..6a4d219 100644
--- a/patrole_tempest_plugin/tests/unit/test_policy_authority.py
+++ b/patrole_tempest_plugin/tests/unit/test_policy_authority.py
@@ -108,9 +108,60 @@
 
         for rule, role_list in expected.items():
             for role in role_list:
-                self.assertTrue(authority.allowed(rule, role))
+                self.assertTrue(authority.allowed(rule, [role]))
             for role in set(default_roles) - set(role_list):
-                self.assertFalse(authority.allowed(rule, role))
+                self.assertFalse(authority.allowed(rule, [role]))
+
+    @mock.patch.object(policy_authority, 'LOG', autospec=True)
+    def _test_custom_multi_roles_policy(self, *args):
+        default_roles = ['zero', 'one', 'two', 'three', 'four',
+                         'five', 'six', 'seven', 'eight', 'nine']
+
+        test_tenant_id = mock.sentinel.tenant_id
+        test_user_id = mock.sentinel.user_id
+        authority = policy_authority.PolicyAuthority(
+            test_tenant_id, test_user_id, "custom_rbac_policy")
+
+        expected = {
+            'policy_action_1': ['two', 'four', 'six', 'eight'],
+            'policy_action_2': ['one', 'three', 'five', 'seven', 'nine'],
+            'policy_action_4': ['one', 'two', 'three', 'five', 'seven'],
+            'policy_action_5': ['zero', 'one', 'two', 'three', 'four', 'five',
+                                'six', 'seven', 'eight', 'nine'],
+        }
+
+        for rule, role_list in expected.items():
+            allowed_roles_lists = [roles for roles in [
+                role_list[len(role_list) // 2:],
+                role_list[:len(role_list) // 2]] if roles]
+            for test_roles in allowed_roles_lists:
+                self.assertTrue(authority.allowed(rule, test_roles))
+
+            disallowed_roles = list(set(default_roles) - set(role_list))
+            disallowed_roles_lists = [roles for roles in [
+                disallowed_roles[len(disallowed_roles) // 2:],
+                disallowed_roles[:len(disallowed_roles) // 2]] if roles]
+            for test_roles in disallowed_roles_lists:
+                self.assertFalse(authority.allowed(rule, test_roles))
+
+    def test_empty_rbac_test_roles(self):
+        test_tenant_id = mock.sentinel.tenant_id
+        test_user_id = mock.sentinel.user_id
+        authority = policy_authority.PolicyAuthority(
+            test_tenant_id, test_user_id, "custom_rbac_policy")
+
+        disallowed_for_empty_roles = ['policy_action_1', 'policy_action_2',
+                                      'policy_action_3', 'policy_action_4',
+                                      'policy_action_6']
+
+        # Due to "policy_action_5": "rule:all_rule" / "all_rule": ""
+        allowed_for_empty_roles = ['policy_action_5']
+
+        for rule in disallowed_for_empty_roles:
+            self.assertFalse(authority.allowed(rule, []))
+
+        for rule in allowed_for_empty_roles:
+            self.assertTrue(authority.allowed(rule, []))
 
     def test_custom_policy_json(self):
         # The CONF.patrole.custom_policy_files has a path to JSON file by
@@ -122,24 +173,34 @@
             custom_policy_files=[self.conf_policy_path_yaml], group='patrole'))
         self._test_custom_policy()
 
+    def test_custom_multi_roles_policy_json(self):
+        # The CONF.patrole.custom_policy_files has a path to JSON file by
+        # default, so we don't need to use ConfPatcher here.
+        self._test_custom_multi_roles_policy()
+
+    def test_custom_multi_roles_policy_yaml(self):
+        self.useFixture(fixtures.ConfPatcher(
+            custom_policy_files=[self.conf_policy_path_yaml], group='patrole'))
+        self._test_custom_multi_roles_policy()
+
     def test_admin_policy_file_with_admin_role(self):
         test_tenant_id = mock.sentinel.tenant_id
         test_user_id = mock.sentinel.user_id
         authority = policy_authority.PolicyAuthority(
             test_tenant_id, test_user_id, "admin_rbac_policy")
 
-        role = 'admin'
+        roles = ['admin']
         allowed_rules = [
             'admin_rule', 'is_admin_rule', 'alt_admin_rule'
         ]
         disallowed_rules = ['non_admin_rule']
 
         for rule in allowed_rules:
-            allowed = authority.allowed(rule, role)
+            allowed = authority.allowed(rule, roles)
             self.assertTrue(allowed)
 
         for rule in disallowed_rules:
-            allowed = authority.allowed(rule, role)
+            allowed = authority.allowed(rule, roles)
             self.assertFalse(allowed)
 
     def test_admin_policy_file_with_member_role(self):
@@ -148,7 +209,7 @@
         authority = policy_authority.PolicyAuthority(
             test_tenant_id, test_user_id, "admin_rbac_policy")
 
-        role = 'Member'
+        roles = ['Member']
         allowed_rules = [
             'non_admin_rule'
         ]
@@ -156,11 +217,11 @@
             'admin_rule', 'is_admin_rule', 'alt_admin_rule']
 
         for rule in allowed_rules:
-            allowed = authority.allowed(rule, role)
+            allowed = authority.allowed(rule, roles)
             self.assertTrue(allowed)
 
         for rule in disallowed_rules:
-            allowed = authority.allowed(rule, role)
+            allowed = authority.allowed(rule, roles)
             self.assertFalse(allowed)
 
     def test_alt_admin_policy_file_with_context_is_admin(self):
@@ -169,28 +230,28 @@
         authority = policy_authority.PolicyAuthority(
             test_tenant_id, test_user_id, "alt_admin_rbac_policy")
 
-        role = 'fake_admin'
+        roles = ['fake_admin']
         allowed_rules = ['non_admin_rule']
         disallowed_rules = ['admin_rule']
 
         for rule in allowed_rules:
-            allowed = authority.allowed(rule, role)
+            allowed = authority.allowed(rule, roles)
             self.assertTrue(allowed)
 
         for rule in disallowed_rules:
-            allowed = authority.allowed(rule, role)
+            allowed = authority.allowed(rule, roles)
             self.assertFalse(allowed)
 
-        role = 'super_admin'
+        roles = ['super_admin']
         allowed_rules = ['admin_rule']
         disallowed_rules = ['non_admin_rule']
 
         for rule in allowed_rules:
-            allowed = authority.allowed(rule, role)
+            allowed = authority.allowed(rule, roles)
             self.assertTrue(allowed)
 
         for rule in disallowed_rules:
-            allowed = authority.allowed(rule, role)
+            allowed = authority.allowed(rule, roles)
             self.assertFalse(allowed)
 
     def test_tenant_user_policy(self):
@@ -208,17 +269,17 @@
         # Check whether Member role can perform expected actions.
         allowed_rules = ['rule1', 'rule2', 'rule3', 'rule4']
         for rule in allowed_rules:
-            allowed = authority.allowed(rule, 'Member')
+            allowed = authority.allowed(rule, ['Member'])
             self.assertTrue(allowed)
 
         disallowed_rules = ['admin_tenant_rule', 'admin_user_rule']
         for disallowed_rule in disallowed_rules:
-            self.assertFalse(authority.allowed(disallowed_rule, 'Member'))
+            self.assertFalse(authority.allowed(disallowed_rule, ['Member']))
 
         # Check whether admin role can perform expected actions.
         allowed_rules.extend(disallowed_rules)
         for rule in allowed_rules:
-            allowed = authority.allowed(rule, 'admin')
+            allowed = authority.allowed(rule, ['admin'])
             self.assertTrue(allowed)
 
         # Check whether _try_rule is called with the correct target dictionary.
@@ -243,7 +304,7 @@
             }
 
             for rule in allowed_rules:
-                allowed = authority.allowed(rule, 'Member')
+                allowed = authority.allowed(rule, ['Member'])
                 self.assertTrue(allowed)
                 mock_try_rule.assert_called_once_with(
                     rule, expected_target, expected_access_data, mock.ANY)
@@ -292,7 +353,7 @@
             fake_rule, [self.custom_policy_file], "custom_rbac_policy")
 
         e = self.assertRaises(rbac_exceptions.RbacParsingException,
-                              authority.allowed, fake_rule, None)
+                              authority.allowed, fake_rule, [None])
         self.assertIn(expected_message, str(e))
         m_log.debug.assert_called_once_with(expected_message)
 
@@ -309,13 +370,13 @@
                mock.sentinel.error)})
 
         expected_message = (
-            'Policy action "{0}" not found in policy files: {1} or among '
+            'Policy action "[{0}]" not found in policy files: {1} or among '
             'registered policy in code defaults for {2} service.').format(
             mock.sentinel.rule, [self.custom_policy_file],
             "custom_rbac_policy")
 
         e = self.assertRaises(rbac_exceptions.RbacParsingException,
-                              authority.allowed, mock.sentinel.rule, None)
+                              authority.allowed, [mock.sentinel.rule], [None])
         self.assertIn(expected_message, str(e))
         m_log.debug.assert_called_once_with(expected_message)
 
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 1a2c691..73a34fc 100644
--- a/patrole_tempest_plugin/tests/unit/test_rbac_rule_validation.py
+++ b/patrole_tempest_plugin/tests/unit/test_rbac_rule_validation.py
@@ -46,8 +46,33 @@
                                project_id=mock.sentinel.project_id)
         setattr(self.mock_test_args.os_primary, 'credentials', mock_creds)
 
+        self.test_roles = ['member']
         self.useFixture(
-            patrole_fixtures.ConfPatcher(rbac_test_role='Member',
+            patrole_fixtures.ConfPatcher(rbac_test_roles=self.test_roles,
+                                         group='patrole'))
+        # Disable patrole log for unit tests.
+        self.useFixture(
+            patrole_fixtures.ConfPatcher(enable_reporting=False,
+                                         group='patrole_log'))
+
+
+class BaseRBACMultiRoleRuleValidationTest(base.TestCase):
+
+    def setUp(self):
+        super(BaseRBACMultiRoleRuleValidationTest, self).setUp()
+        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)
+
+        # 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.test_roles = ['member', 'anotherrole']
+        self.useFixture(
+            patrole_fixtures.ConfPatcher(rbac_test_roles=self.test_roles,
                                          group='patrole'))
         # Disable patrole log for unit tests.
         self.useFixture(
@@ -118,8 +143,8 @@
         def test_policy(*args):
             raise exceptions.Forbidden()
 
-        test_re = ("Role Member was not allowed to perform the following "
-                   "actions: \[%s\].*" % (mock.sentinel.action))
+        test_re = ("User with roles \['member'\] was not allowed to perform "
+                   "the following actions: \[%s\].*" % (mock.sentinel.action))
         self.assertRaisesRegex(
             rbac_exceptions.RbacUnderPermissionException, test_re, test_policy,
             self.mock_test_args)
@@ -127,44 +152,66 @@
 
     @mock.patch.object(rbac_rv, 'LOG', autospec=True)
     @mock.patch.object(rbac_rv, 'policy_authority', autospec=True)
-    def test_rule_validation_rbac_malformed_response_positive(
+    def test_rule_validation_rbac_failed_response_body_positive(
             self, mock_authority, mock_log):
-        """Test RbacMalformedResponse error is thrown without permission
-        passes.
+        """Test BasePatroleResponseBodyException error is thrown without
+        permission passes.
 
-        Positive test case: if RbacMalformedResponse is thrown and the user is
-        not allowed to perform the action, then this is a success.
+        Positive test case: if subclass of BasePatroleResponseBodyException is
+        thrown and the user is not allowed to perform the action, then this is
+        a success.
         """
         mock_authority.PolicyAuthority.return_value.allowed.return_value =\
             False
 
-        @rbac_rv.action(mock.sentinel.service, rules=[mock.sentinel.action])
-        def test_policy(*args):
-            raise rbac_exceptions.RbacMalformedResponse()
+        def _do_test(exception_cls, **kwargs):
+            @rbac_rv.action(mock.sentinel.service,
+                            rules=[mock.sentinel.action])
+            def test_policy(*args):
+                raise exception_cls(**kwargs)
 
-        mock_log.error.assert_not_called()
+            mock_log.error.assert_not_called()
+            mock_log.warning.assert_not_called()
+
+        _do_test(rbac_exceptions.RbacMissingAttributeResponseBody,
+                 attribute=mock.sentinel.attr)
+        _do_test(rbac_exceptions.RbacPartialResponseBody,
+                 body=mock.sentinel.body)
+        _do_test(rbac_exceptions.RbacEmptyResponseBody)
 
     @mock.patch.object(rbac_rv, 'LOG', autospec=True)
     @mock.patch.object(rbac_rv, 'policy_authority', autospec=True)
-    def test_rule_validation_rbac_malformed_response_negative(
+    def test_rule_validation_soft_authorization_exceptions(
             self, mock_authority, mock_log):
-        """Test RbacMalformedResponse error is thrown with permission fails.
+        """Test RbacUnderPermissionException error is thrown when any of the
+        soft authorization-related exceptions are raised by a test.
 
-        Negative test case: if RbacMalformedResponse is thrown and the user is
-        allowed to perform the action, then this is an expected failure.
+        Negative test case: if subclass of BasePatroleResponseBodyException is
+        thrown and the user is allowed to perform the action, then this is an
+        expected failure.
         """
         mock_authority.PolicyAuthority.return_value.allowed.return_value = True
 
-        @rbac_rv.action(mock.sentinel.service, rules=[mock.sentinel.action])
-        def test_policy(*args):
-            raise rbac_exceptions.RbacMalformedResponse()
+        def _do_test(exception_cls, **kwargs):
+            @rbac_rv.action(mock.sentinel.service,
+                            rules=[mock.sentinel.action])
+            def test_policy(*args):
+                raise exception_cls(**kwargs)
 
-        test_re = ("Role Member was not allowed to perform the following "
-                   "actions: \[%s\].*" % (mock.sentinel.action))
-        self.assertRaisesRegex(
-            rbac_exceptions.RbacUnderPermissionException, test_re, test_policy,
-            self.mock_test_args)
-        self.assertRegex(mock_log.error.mock_calls[0][1][0], test_re)
+            test_re = (".*User with roles \[%s\] was not allowed to "
+                       "perform the following actions: \[%s\].*" % (
+                           ', '.join("'%s'" % r for r in self.test_roles),
+                           mock.sentinel.action))
+            self.assertRaisesRegex(
+                rbac_exceptions.RbacUnderPermissionException, test_re,
+                test_policy, self.mock_test_args)
+            self.assertRegex(mock_log.error.mock_calls[0][1][0], test_re)
+
+        _do_test(rbac_exceptions.RbacMissingAttributeResponseBody,
+                 attribute=mock.sentinel.attr)
+        _do_test(rbac_exceptions.RbacPartialResponseBody,
+                 body=mock.sentinel.body)
+        _do_test(rbac_exceptions.RbacEmptyResponseBody)
 
     @mock.patch.object(rbac_rv, 'LOG', autospec=True)
     @mock.patch.object(rbac_rv, 'policy_authority', autospec=True)
@@ -214,8 +261,8 @@
             raise exceptions.NotFound()
 
         expected_errors = [
-            ("Role Member was not allowed to perform the following "
-             "actions: \['%s'\].*" % policy_names[0]),
+            ("User with roles \['member'\] was not allowed to perform the "
+             "following actions: \['%s'\].*" % policy_names[0]),
             None
         ]
 
@@ -348,6 +395,86 @@
             mock_log.error.reset_mock()
 
 
+class RBACMultiRoleRuleValidationTest(BaseRBACMultiRoleRuleValidationTest,
+                                      RBACRuleValidationTest):
+    @mock.patch.object(rbac_rv, 'LOG', autospec=True)
+    @mock.patch.object(rbac_rv, 'policy_authority', autospec=True)
+    def test_rule_validation_forbidden_negative(self, mock_authority,
+                                                mock_log):
+        """Test RbacUnderPermissionException error is thrown and have
+        permission fails.
+
+        Negative test case: if Forbidden is thrown and the user should be
+        allowed to perform the action, then the RbacUnderPermissionException
+        exception should be raised.
+        """
+        mock_authority.PolicyAuthority.return_value.allowed.return_value = True
+
+        @rbac_rv.action(mock.sentinel.service, rules=[mock.sentinel.action])
+        def test_policy(*args):
+            raise exceptions.Forbidden()
+
+        test_re = ("User with roles \['member', 'anotherrole'\] was not "
+                   "allowed to perform the following actions: \[%s\].*" %
+                   (mock.sentinel.action))
+        self.assertRaisesRegex(
+            rbac_exceptions.RbacUnderPermissionException, test_re, test_policy,
+            self.mock_test_args)
+        self.assertRegex(mock_log.error.mock_calls[0][1][0], test_re)
+
+    @mock.patch.object(rbac_rv, 'LOG', autospec=True)
+    @mock.patch.object(rbac_rv, 'policy_authority', autospec=True)
+    def test_expect_not_found_and_raise_not_found(self, mock_authority,
+                                                  mock_log):
+        """Test that expecting 404 and getting 404 works for all scenarios.
+
+        Tests the following scenarios:
+        1) Test no permission and 404 is expected and 404 is thrown succeeds.
+        2) Test have permission and 404 is expected and 404 is thrown fails.
+
+        In both cases, a LOG.warning is called with the "irregular message"
+        that signals to user that a 404 was expected and caught.
+        """
+        policy_names = ['foo:bar']
+
+        @rbac_rv.action(mock.sentinel.service, rules=policy_names,
+                        expected_error_codes=[404])
+        def test_policy(*args):
+            raise exceptions.NotFound()
+
+        expected_errors = [
+            ("User with roles \['member', 'anotherrole'\] was not allowed to "
+             "perform the following actions: \['%s'\].*" % policy_names[0]),
+            None
+        ]
+
+        for pos, allowed in enumerate([True, False]):
+            mock_authority.PolicyAuthority.return_value.allowed\
+                .return_value = allowed
+
+            error_re = expected_errors[pos]
+
+            if error_re:
+                self.assertRaisesRegex(
+                    rbac_exceptions.RbacUnderPermissionException, error_re,
+                    test_policy, self.mock_test_args)
+                self.assertRegex(mock_log.error.mock_calls[0][1][0], error_re)
+            else:
+                test_policy(self.mock_test_args)
+                mock_log.error.assert_not_called()
+
+            mock_log.warning.assert_called_with(
+                "NotFound exception was caught for test %s. Expected policies "
+                "which may have caused the error: %s. The service %s throws a "
+                "404 instead of a 403, which is irregular",
+                test_policy.__name__,
+                ', '.join(policy_names),
+                mock.sentinel.service)
+
+            mock_log.warning.reset_mock()
+            mock_log.error.reset_mock()
+
+
 class RBACRuleValidationLoggingTest(BaseRBACRuleValidationTest):
     """Test class for validating the RBAC log, dedicated to just logging
     Patrole RBAC validation work flows.
@@ -422,7 +549,7 @@
         policy_authority = mock_authority.PolicyAuthority.return_value
         policy_authority.allowed.assert_called_with(
             mock.sentinel.action,
-            CONF.patrole.rbac_test_role)
+            CONF.patrole.rbac_test_roles)
 
         mock_log.error.assert_not_called()
 
@@ -454,18 +581,23 @@
         policy_authority = mock_authority.PolicyAuthority.return_value
         policy_authority.allowed.assert_called_with(
             "foo",
-            CONF.patrole.rbac_test_role)
+            CONF.patrole.rbac_test_roles)
         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)
+            CONF.patrole.rbac_test_roles)
 
         mock_log.error.assert_not_called()
 
 
+class RBACMultiRoleRuleValidationLoggingTest(
+    BaseRBACMultiRoleRuleValidationTest, RBACRuleValidationLoggingTest):
+    pass
+
+
 class RBACRuleValidationNegativeTest(BaseRBACRuleValidationTest):
 
     def setUp(self):
@@ -488,6 +620,11 @@
                           test_policy, self.mock_test_args)
 
 
+class RBACMultiRoleRuleValidationNegativeTest(
+    BaseRBACMultiRoleRuleValidationTest, RBACRuleValidationNegativeTest):
+    pass
+
+
 class RBACRuleValidationTestMultiPolicy(BaseRBACRuleValidationTest):
     """Test suite for validating multi-policy support for the
     ``rbac_rule_validation`` decorator.
@@ -502,8 +639,9 @@
     def _assert_policy_authority_called_with(self, rules, mock_authority):
         m_authority = mock_authority.PolicyAuthority.return_value
         m_authority.allowed.assert_has_calls([
-            mock.call(rule, CONF.patrole.rbac_test_role) for rule in rules
+            mock.call(rule, CONF.patrole.rbac_test_roles) for rule in rules
         ])
+        m_authority.allowed.reset_mock()
 
     @mock.patch.object(rbac_rv, 'policy_authority', autospec=True)
     def test_rule_validation_multi_policy_have_permission_success(
@@ -614,10 +752,10 @@
         mock_authority.PolicyAuthority.return_value.allowed\
             .return_value = True
 
-        error_re = ("Role Member was not allowed to perform the following "
-                    "actions: %s. Expected allowed actions: %s. Expected "
-                    "disallowed actions: []." % (rules, rules)).replace(
-                        '[', '\[').replace(']', '\]')
+        error_re = ("User with roles ['member'] was not allowed to perform "
+                    "the following actions: %s. Expected allowed actions: %s. "
+                    "Expected disallowed actions: []." %
+                    (rules, rules)).replace('[', '\[').replace(']', '\]')
         self.assertRaisesRegex(
             rbac_exceptions.RbacUnderPermissionException, error_re,
             test_policy, self.mock_test_args)
@@ -689,6 +827,44 @@
         _do_test([True, False, False, True], 'mock.sentinel.action2')
         _do_test([True, False, True, False], 'mock.sentinel.action2')
 
+    @mock.patch.object(rbac_rv, 'LOG', autospec=True)
+    @mock.patch.object(rbac_rv, 'policy_authority', autospec=True)
+    def test_rule_validation_multi_policy_defaults_to_correct_error_codes(
+            self, mock_authority, mock_log):
+        """Test omission of expected_error_codes defaults to [403] * len(rules)
+        """
+        mock_authority.PolicyAuthority.return_value.allowed.\
+            return_value = False
+        expected_log = "%s: Expecting %d to be raised for policy name: %s"
+
+        # Validate with single rule => expected_error_codes == [403].
+        rules = [mock.sentinel.action1]
+
+        @rbac_rv.action(mock.sentinel.service, rules=rules)
+        def test_policy(*args):
+            raise exceptions.Forbidden()
+
+        test_policy(self.mock_test_args)
+        self._assert_policy_authority_called_with(rules, mock_authority)
+        # Assert that 403 is expected.
+        mock_calls = [x[1] for x in mock_log.debug.mock_calls]
+        self.assertTrue(
+            any([(expected_log, 'test_policy', 403, rules[0]) in mock_calls]))
+
+        # Validate with multiple rules => expected_error_codes == [403, 403].
+        rules = [mock.sentinel.action1, mock.sentinel.action2]
+
+        @rbac_rv.action(mock.sentinel.service, rules=rules)
+        def test_policy(*args):
+            raise exceptions.Forbidden()
+
+        test_policy(self.mock_test_args)
+        self._assert_policy_authority_called_with(rules, mock_authority)
+        # Assert that 403 is expected.
+        mock_calls = [x[1] for x in mock_log.debug.mock_calls]
+        self.assertTrue(
+            any([(expected_log, 'test_policy', 403, rules[0]) in mock_calls]))
+
     def test_prepare_multi_policy_allowed_usages(self):
 
         def _do_test(rules, ecodes, exp_rules, exp_ecodes):
@@ -739,6 +915,39 @@
         self.assertRaisesRegex(ValueError, error_re, _do_test, None, [404])
 
 
+class RBACMultiRoleRuleValidationTestMultiPolicy(
+    BaseRBACMultiRoleRuleValidationTest, RBACRuleValidationTestMultiPolicy):
+    @mock.patch.object(rbac_rv, 'LOG', autospec=True)
+    @mock.patch.object(rbac_rv, 'policy_authority', autospec=True)
+    def test_rule_validation_multi_policy_forbidden_failure(
+            self, mock_authority, mock_log):
+        """Test that when the expected result is authorized and the test
+        fails (with a Forbidden error code) that the overall evaluation
+        results in a RbacUnderPermissionException getting raised.
+        """
+
+        # NOTE: Avoid mock.sentinel here due to weird sorting with them.
+        rules = ['action1', 'action2', 'action3']
+
+        @rbac_rv.action(mock.sentinel.service, rules=rules,
+                        expected_error_codes=[403, 403, 403])
+        def test_policy(*args):
+            raise exceptions.Forbidden()
+
+        mock_authority.PolicyAuthority.return_value.allowed\
+            .return_value = True
+
+        error_re = ("User with roles ['member', 'anotherrole'] was not "
+                    "allowed to perform the following actions: %s. Expected "
+                    "allowed actions: %s. Expected disallowed actions: []." %
+                    (rules, rules)).replace('[', '\[').replace(']', '\]')
+        self.assertRaisesRegex(
+            rbac_exceptions.RbacUnderPermissionException, error_re,
+            test_policy, self.mock_test_args)
+        self.assertRegex(mock_log.error.mock_calls[0][1][0], error_re)
+        self._assert_policy_authority_called_with(rules, mock_authority)
+
+
 class RBACOverrideRoleValidationTest(BaseRBACRuleValidationTest):
     """Class for validating that untimely exceptions (outside
     ``override_role`` is called) result in test failures.
@@ -793,7 +1002,7 @@
     def test_rule_validation_override_role_patrole_exception_ignored(
             self, mock_authority):
         """Test success case where Patrole exception is raised (which is
-        valid in case of e.g. RbacMalformedException) after override_role
+        valid in case of e.g. RbacPartialResponseBody) after override_role
         passes.
         """
         mock_authority.PolicyAuthority.return_value.allowed.return_value =\
diff --git a/patrole_tempest_plugin/tests/unit/test_rbac_utils.py b/patrole_tempest_plugin/tests/unit/test_rbac_utils.py
index 5132079..bd13e34 100644
--- a/patrole_tempest_plugin/tests/unit/test_rbac_utils.py
+++ b/patrole_tempest_plugin/tests/unit/test_rbac_utils.py
@@ -52,7 +52,7 @@
         self.rbac_utils.override_role()
 
         mock_test_obj = self.rbac_utils.mock_test_obj
-        roles_client = self.rbac_utils.roles_v3_client
+        roles_client = self.rbac_utils.admin_roles_client
         mock_time = self.rbac_utils.mock_time
 
         roles_client.create_user_role_on_project.assert_called_once_with(
@@ -67,7 +67,7 @@
         self.rbac_utils.set_roles(['admin', 'member'], 'admin')
         self.rbac_utils.override_role()
 
-        roles_client = self.rbac_utils.roles_v3_client
+        roles_client = self.rbac_utils.admin_roles_client
         mock_time = self.rbac_utils.mock_time
 
         roles_client.create_user_role_on_project.assert_not_called()
@@ -77,7 +77,7 @@
         self.rbac_utils.override_role(True)
 
         mock_test_obj = self.rbac_utils.mock_test_obj
-        roles_client = self.rbac_utils.roles_v3_client
+        roles_client = self.rbac_utils.admin_roles_client
         mock_time = self.rbac_utils.mock_time
 
         roles_client.create_user_role_on_project.assert_has_calls([
@@ -96,7 +96,7 @@
         self.rbac_utils.set_roles(['admin', 'member'], 'member')
         self.rbac_utils.override_role(True)
 
-        roles_client = self.rbac_utils.roles_v3_client
+        roles_client = self.rbac_utils.admin_roles_client
         mock_time = self.rbac_utils.mock_time
 
         roles_client.create_user_role_on_project.assert_has_calls([
@@ -109,7 +109,7 @@
         self.rbac_utils.override_role(True, False)
 
         mock_test_obj = self.rbac_utils.mock_test_obj
-        roles_client = self.rbac_utils.roles_v3_client
+        roles_client = self.rbac_utils.admin_roles_client
         mock_time = self.rbac_utils.mock_time
 
         roles_client.create_user_role_on_project.assert_has_calls([
@@ -133,7 +133,7 @@
         self.rbac_utils.set_roles(['admin', 'member'], ['member', 'random'])
         self.rbac_utils.override_role()
 
-        roles_client = self.rbac_utils.roles_v3_client
+        roles_client = self.rbac_utils.admin_roles_client
 
         roles_client.list_user_roles_on_project.assert_called_once_with(
             self.rbac_utils.PROJECT_ID, self.rbac_utils.USER_ID)
@@ -169,7 +169,8 @@
         mock_override_role.assert_called_once_with(_rbac_utils, test_obj,
                                                    False)
 
-    @mock.patch.object(rbac_utils.RbacUtils, '_override_role', autospec=True)
+    @mock.patch.object(rbac_utils.RbacUtils, '_override_role',
+                       autospec=True)
     def test_override_role_context_manager_simulate_fail(self,
                                                          mock_override_role):
         """Validate that expected override_role calls are made when switching
diff --git a/patrole_tempest_plugin/tests/unit/test_requirements_authority.py b/patrole_tempest_plugin/tests/unit/test_requirements_authority.py
index 1fb9636..94af81f 100644
--- a/patrole_tempest_plugin/tests/unit/test_requirements_authority.py
+++ b/patrole_tempest_plugin/tests/unit/test_requirements_authority.py
@@ -17,19 +17,29 @@
 from tempest.lib import exceptions
 from tempest.tests import base
 
+from patrole_tempest_plugin import rbac_exceptions
 from patrole_tempest_plugin import requirements_authority as req_auth
 
 
-class RequirementsAuthorityTest(base.TestCase):
+class BaseRequirementsAuthorityTest(base.TestCase):
     def setUp(self):
-        super(RequirementsAuthorityTest, self).setUp()
+        super(BaseRequirementsAuthorityTest, 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']}
+        self.expected_result = {'test:create': [['test_member'], ['_member_']],
+                                'test:create2': [['test_member']],
+                                'test:create3': [['test_member', '_member_']],
+                                'test:create4': [['test_member', '!_member_']]}
+        self.expected_rbac_map = {'test:create': ['test_member', '_member_'],
+                                  'test:create2': ['test_member'],
+                                  'test:create3': ['test_member, _member_'],
+                                  'test:create4': ['test_member, !_member_']}
+
+
+class RequirementsAuthorityTest(BaseRequirementsAuthorityTest):
 
     def test_requirements_auth_init(self):
         rbac_auth = req_auth.RequirementsAuthority(self.yaml_test_file, 'Test')
@@ -38,37 +48,38 @@
     def test_auth_allowed_empty_roles(self):
         self.rbac_auth.roles_dict = None
         self.assertRaises(exceptions.InvalidConfiguration,
-                          self.rbac_auth.allowed, "", "")
+                          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_"))
+        self.rbac_auth.roles_dict = {'rule': [['_member_']]}
+        self.assertTrue(self.rbac_auth.allowed("rule", ["_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"))
+        self.rbac_auth.roles_dict = {'rule': [['_member_']]}
+        self.assertFalse(self.rbac_auth.allowed("rule", "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_get_allowed_invalid_rule_raises_parsing_exception(self):
+        self.rbac_auth.roles_dict = {"foo": "bar"}
+        self.assertRaises(rbac_exceptions.RbacParsingException,
+                          self.rbac_auth.allowed, "baz", "support_member")
 
     def test_parser_init(self):
         req_auth.RequirementsParser(self.yaml_test_file)
-        self.assertEqual([{'Test': self.expected_result}],
+        self.assertEqual([{'Test': self.expected_rbac_map}],
                          req_auth.RequirementsParser.Inner._rbac_map)
 
     def test_parser_role_in_api(self):
         req_auth.RequirementsParser.Inner._rbac_map = \
-            [{'Test': self.expected_result}]
+            [{'Test': self.expected_rbac_map}]
         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"))
+        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}]
+            [{'Test': self.expected_rbac_map}]
         self.rbac_auth.roles_dict = req_auth.RequirementsParser.parse("Test")
 
         self.assertEqual(self.expected_result, self.rbac_auth.roles_dict)
@@ -76,10 +87,102 @@
 
     def test_parser_except_invalid_configuration(self):
         req_auth.RequirementsParser.Inner._rbac_map = \
-            [{'Test': self.expected_result}]
+            [{'Test': self.expected_rbac_map}]
         self.rbac_auth.roles_dict = \
             req_auth.RequirementsParser.parse("Failure")
 
-        self.assertIsNone(self.rbac_auth.roles_dict)
+        self.assertFalse(self.rbac_auth.roles_dict)
         self.assertRaises(exceptions.InvalidConfiguration,
-                          self.rbac_auth.allowed, "", "")
+                          self.rbac_auth.allowed, "", [""])
+
+    def test_auth_allowed_exclamation_mark_syntax_single_role(self):
+        """Ensure that exclamation mark in front of role is dropped, and not
+        considered as part of role itself.
+        """
+
+        self.rbac_auth.roles_dict = {'rule': [['!admin']]}
+        self.assertTrue(self.rbac_auth.allowed("rule", ["member"]))
+        self.assertTrue(self.rbac_auth.allowed("rule", ["!admin"]))
+        self.assertFalse(self.rbac_auth.allowed("rule", ["admin"]))
+
+
+class RequirementsAuthorityMultiRoleTest(BaseRequirementsAuthorityTest):
+
+    def test_auth_allowed_exclamation_mark_syntax_multi_role(self):
+        """Ensure that exclamation mark in front of role is dropped, and not
+        considered as part of role itself.
+        """
+
+        self.rbac_auth.roles_dict = {'rule': [['member', '!admin']]}
+        self.assertFalse(self.rbac_auth.allowed("rule", ["member", "admin"]))
+        self.assertTrue(self.rbac_auth.allowed("rule", ["member", "!admin"]))
+
+    def test_auth_allowed_single_rule_scenario(self):
+        # member and support and not admin and not manager
+        self.rbac_auth.roles_dict = {'rule': [['member', 'support',
+                                               '!admin', '!manager']]}
+
+        # User is member and support and not manager or admin
+        self.assertTrue(self.rbac_auth.allowed("rule", ["member",
+                                                        "support"]))
+
+        # User is member and not manager or admin, but not support
+        self.assertFalse(self.rbac_auth.allowed("rule", ["member"]))
+
+        # User is support and not manager or admin, but not member
+        self.assertFalse(self.rbac_auth.allowed("rule", ["support"]))
+
+        # User is member and support and not manager, but have admin role
+        self.assertFalse(self.rbac_auth.allowed("rule", ["member",
+                                                         "support",
+                                                         "admin"]))
+
+        # User is member and not manager, but have admin role and not support
+        self.assertFalse(self.rbac_auth.allowed("rule", ["member",
+                                                         "admin"]))
+
+        # User is member and support, but have manager and admin roles
+        self.assertFalse(self.rbac_auth.allowed("rule", ["member",
+                                                         "support",
+                                                         "admin",
+                                                         "manager"]))
+
+    def test_auth_allowed_multi_rule_scenario(self):
+        rules = [
+            ['member', 'support', '!admin', '!manager'],
+            ['member', 'admin'],
+            ["manager"]
+        ]
+        self.rbac_auth.roles_dict = {'rule': rules}
+
+        # Not a single role allows viewer
+        self.assertFalse(self.rbac_auth.allowed("rule", ["viewer"]))
+        # We have no rule that allows support and admin
+        self.assertFalse(self.rbac_auth.allowed("rule", ["support",
+                                                         "admin"]))
+        # There is no rule that requires member without additional requirements
+        self.assertFalse(self.rbac_auth.allowed("rule", ["member"]))
+
+        # Pass with rules[2]
+        self.assertTrue(self.rbac_auth.allowed("rule", ["manager"]))
+        # Pass with rules[0]
+        self.assertTrue(self.rbac_auth.allowed("rule", ["member",
+                                                        "support"]))
+        # Pass with rules[1]
+        self.assertTrue(self.rbac_auth.allowed("rule", ["member",
+                                                        "admin"]))
+        # Pass with rules[2]
+        self.assertTrue(self.rbac_auth.allowed("rule", ["manager",
+                                                        "admin"]))
+        # Pass with rules[1]
+        self.assertTrue(self.rbac_auth.allowed("rule", ["member",
+                                                        "support",
+                                                        "admin"]))
+        # Pass with rules[1]
+        self.assertTrue(self.rbac_auth.allowed("rule", ["member",
+                                                        "support",
+                                                        "admin",
+                                                        "manager"]))
+        # Pass with rules[2]
+        self.assertTrue(self.rbac_auth.allowed("rule", ["admin",
+                                                        "manager"]))
diff --git a/releasenotes/notes/break-up-rbac-malformed-exception-into-discrete-exceptions-92aedb99d0a13f58.yaml b/releasenotes/notes/break-up-rbac-malformed-exception-into-discrete-exceptions-92aedb99d0a13f58.yaml
new file mode 100644
index 0000000..0a93b64
--- /dev/null
+++ b/releasenotes/notes/break-up-rbac-malformed-exception-into-discrete-exceptions-92aedb99d0a13f58.yaml
@@ -0,0 +1,25 @@
+---
+features:
+  - |
+    The exception class ``RbacMalformedException`` has been broken up into the
+    following discrete exceptions:
+
+    * ``RbacMissingAttributeResponseBody`` - incomplete means that the
+      response body (for show or list) is missing certain attributes
+    * ``RbacPartialResponseBody`` - partial means that a list response
+      only returned a subset of the possible results available.
+    * ``RbacEmptyResponseBody`` - empty means that the show or list
+      response body is entirely empty
+
+    Each of the exception classes above deals with a different type of failure
+    related to a soft authorization failure. This means that, rather than a
+    403 error code getting returned by the server, the response body is
+    incomplete in some way.
+upgrade:
+  - |
+    The exception class ``RbacMalformedException`` has been removed. Use one
+    of the following exception classes instead:
+
+    * ``RbacMissingAttributeResponseBody``
+    * ``RbacPartialResponseBody``
+    * ``RbacEmptyResponseBody``
diff --git a/releasenotes/notes/multi-role-rbac-7f597c004a558956.yaml b/releasenotes/notes/multi-role-rbac-7f597c004a558956.yaml
new file mode 100644
index 0000000..20c6c0e
--- /dev/null
+++ b/releasenotes/notes/multi-role-rbac-7f597c004a558956.yaml
@@ -0,0 +1,11 @@
+---
+features:
+  - |
+    We have replaced CONF.patrole.rbac_test_role with
+    CONF.patrole.rbac_test_roles, where instead of single role we can specify
+    list of roles to be assigned to test user. This way we may run rbac tests
+    for scenarios that requires user to have more that a single role.
+deprecations:
+  - |
+    Config parameter CONF.rbac_test_role is deprecated in favor of
+    CONF.rbac_test_roles that implements a list of roles instead of single role.
diff --git a/releasenotes/notes/requirements-authority-multi-role-support-0fe53fc49567e595.yaml b/releasenotes/notes/requirements-authority-multi-role-support-0fe53fc49567e595.yaml
new file mode 100644
index 0000000..ffbae0a
--- /dev/null
+++ b/releasenotes/notes/requirements-authority-multi-role-support-0fe53fc49567e595.yaml
@@ -0,0 +1,37 @@
+---
+features:
+  - |
+    The ``requirements_authority`` module now supports the following 3 cases:
+
+    * logical or operation of roles (existing functionality)
+    * logical and operation of roles (new functionality)
+    * logical not operation of roles (new functionality)
+
+    .. code-block:: yaml
+
+        <service_foo>:
+          <logical_or_example>:
+            - <allowed_role_1>
+            - <allowed_role_2>
+          <logical_and_example>:
+            - <allowed_role_3>, <allowed_role_4>
+        <service_bar>:
+          <logical_not_example>:
+            - <!disallowed_role_5>
+
+    Each item under ``logical_or_example`` is "logical OR"-ed together. Each
+    role in the comma-separated string under ``logical_and_example`` is
+    "logical AND"-ed together. And each item prefixed with "!" under
+    ``logical_not_example`` is "logical negated".
+
+    This allows for expressing many more complex cases using the
+    ``requirements_authority`` YAML syntax. For example, the policy rule
+    (i.e. what may exist in a ``policy.yaml`` file)::
+
+        "foo_rule: (role:a and not role:b) or role:c"
+
+    May now be expressed using the YAML syntax as::
+
+        foo_rule:
+            - a, !b
+            - c
diff --git a/setup.cfg b/setup.cfg
index 02ce831..77a039a 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -26,20 +26,6 @@
 [upload_sphinx]
 upload-dir = doc/build/html
 
-[compile_catalog]
-directory = patrole/locale
-domain = patrole
-
-[update_catalog]
-domain = patrole
-output_dir = patrole/locale
-input_file = patrole/locale/patrole.pot
-
-[extract_messages]
-keywords = _ gettext ngettext l_ lazy_gettext
-mapping_file = babel.cfg
-output_file = patrole/locale/patrole.pot
-
 [build_releasenotes]
 all_files = 1
 build-dir = releasenotes/build