Merge "multi policy: Fix logging issues with multiple policies"
diff --git a/.zuul.yaml b/.zuul.yaml
index 60f0d05..21b5679 100644
--- a/.zuul.yaml
+++ b/.zuul.yaml
@@ -29,8 +29,20 @@
 
 - job:
     name: patrole-base-multinode
-    parent: legacy-dsvm-base-multinode
+    parent: tempest-multinode-full
+    description: |-
+      Patrole base job for multinode and "slow" tests where "slow" tests include:
+
+      * Tests that take more than ~30 seconds to run.
+      * Tests that experience spurious failures related to servers, volumes,
+        backups and similar resources failing to build.
     timeout: 7800
+    branches:
+      - master
+    required-projects:
+      - openstack-infra/devstack-gate
+      - openstack/tempest
+      - openstack/patrole
     irrelevant-files:
       - ^(test-|)requirements.txt$
       - ^.*\.rst$
@@ -38,10 +50,17 @@
       - ^patrole/patrole_tempest_plugin/tests/unit/.*$
       - ^releasenotes/.*
       - ^setup.cfg$
-    required-projects:
-      - openstack-infra/devstack-gate
-      - openstack/patrole
-      - openstack/tempest
+    vars:
+      devstack_localrc:
+        TEMPEST_PLUGINS: "'{{ ansible_user_dir }}/src/git.openstack.org/openstack/patrole'"
+      devstack_plugins:
+        patrole: git://git.openstack.org/openstack/patrole.git
+      devstack_services:
+        tempest: true
+        neutron: true
+      tempest_concurrency: 1
+      tempest_test_regex: (?=.*\[.*\bslow\b.*\])(^patrole_tempest_plugin\.tests\.api)
+      tox_envlist: all-plugin
 
 - job:
     name: patrole-admin
@@ -77,18 +96,18 @@
 - job:
     name: patrole-multinode-admin
     parent: patrole-base-multinode
-    run: playbooks/patrole-multinode-admin/run.yaml
-    post-run: playbooks/patrole-multinode-admin/post.yaml
     voting: false
-    nodeset: legacy-ubuntu-xenial-2-node
+    vars:
+      devstack_localrc:
+        RBAC_TEST_ROLE: admin
 
 - job:
     name: patrole-multinode-member
     parent: patrole-base-multinode
-    run: playbooks/patrole-multinode-member/run.yaml
-    post-run: playbooks/patrole-multinode-member/post.yaml
     voting: false
-    nodeset: legacy-ubuntu-xenial-2-node
+    vars:
+      devstack_localrc:
+        RBAC_TEST_ROLE: member
 
 - job:
     name: patrole-py35-member
diff --git a/README.rst b/README.rst
index 0c786b9..fb8976f 100644
--- a/README.rst
+++ b/README.rst
@@ -33,10 +33,22 @@
 * *Atomicity*. Patrole tests should be atomic: they should test policies in
   isolation. Unlike Tempest, a Patrole test strives to only call a single
   endpoint at a time.
-* *Holistic coverage*. Patrole strives for complete coverage of the OpenStack
-  API. Additionally, Patrole strives to test the API-to-policy mapping
-  contained in each project's policy in code documentation.
-* *Self-contained*. Patrole should attempt to clean up after itself; whenever
+* *Complete coverage*. Patrole should validate all policy in code defaults. For
+  testing, Patrole uses the API-to-policy mapping contained in each project's
+  `policy in code`_ documentation where applicable.
+
+  For example, Nova's policy in code documentation is located in the
+  `Nova repository`_ under ``nova/policies``. Likewise, Keystone's policy in
+  code documentation is located in the `Keystone repository`_ under
+  ``keystone/common/policies``. The other OpenStack services follow the same
+  directory layout pattern with respect to policy in code.
+
+  .. note::
+
+    Realistically this is not always possible because some services have
+    not yet moved to policy in code.
+
+* *Self-cleaning*. Patrole should attempt to clean up after itself; whenever
   possible we should tear down resources when done.
 
   .. note::
@@ -45,7 +57,11 @@
       pre-provisioned credentials. Work is currently underway to clean up
       modifications made to pre-provisioned credentials.
 
-* *Self-tested*. Patrole should be self-tested.
+* *Self-testing*. Patrole should be self-testing.
+
+.. _policy in code: https://specs.openstack.org/openstack/oslo-specs/specs/newton/policy-in-code.html
+.. _Nova repository: https://github.com/openstack/nova/tree/master/nova/policies
+.. _Keystone repository: https://github.com/openstack/keystone/tree/master/keystone/common/policies
 
 Features
 --------
diff --git a/doc/source/HACKING.rst b/doc/source/HACKING.rst
index 1847447..8777875 100644
--- a/doc/source/HACKING.rst
+++ b/doc/source/HACKING.rst
@@ -1,4 +1,5 @@
-=======
-Hacking
-=======
+====================
+Patrole Coding Guide
+====================
+
 .. include:: ../../HACKING.rst
diff --git a/doc/source/field_guide/rbac.rst b/doc/source/field_guide/rbac.rst
index 2654d31..d2f4af0 100644
--- a/doc/source/field_guide/rbac.rst
+++ b/doc/source/field_guide/rbac.rst
@@ -53,6 +53,48 @@
 * Patrole is designed to work with any role via configuration settings, but
   on the other hand the projects handpick which roles to test.
 
+Why not use Patrole framework on Tempest tests?
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+The Patrole framework can't be applied to existing Tempest tests via
+:ref:`rbac-validation`, because:
+
+* Tempest tests aren't factored the right way: They're not granular enough.
+  They call too many APIs and too many policies are enforced by each test.
+* Tempest tests assume default policy rules: Tempest uses ``os_admin``
+  credentials for admin APIs and ``os_primary`` for non-admin APIs.
+  This breaks for custom policy overrides.
+* Tempest doesn't have tests that enforce all the policy actions, regardless.
+  Some RBAC tests require that tests be written a very precise way for the
+  server to authorize the expected policy actions.
+
+Why are these tests not in Tempest?
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Patrole should be a separate project that specializes in RBAC tests.
+
+Philosophically speaking:
+
+* Tempest supports `API and scenario testing`_. RBAC testing is out of scope.
+* The `OpenStack project structure reform`_ evolved OpenStack "to a more
+  decentralized model where [projects like QA] provide processes and tools to
+  empower projects to do the work themselves". This model resulted in the
+  creation of the `Tempest external plugin interface`_.
+* Tempest supports `plugins`_. Why not use one for RBAC testing?
+
+Practically speaking:
+
+* The Tempest team should not be burdened with having to support Patrole, too.
+  Tempest is a big project and having to absorb RBAC testing is difficult.
+* Tempest already has many in-tree Zuul checks/gates. If Patrole tests lived
+  in Tempest, then adding more Zuul checks/gates for Patrole would only make it
+  harder to get changes merged in Tempest.
+
+.. _API and scenario testing: https://docs.openstack.org/tempest/latest/overview.html#tempest-the-openstack-integration-test-suite
+.. _OpenStack project structure reform: https://governance.openstack.org/tc/resolutions/20141202-project-structure-reform-spec.html#impact-for-horizontal-teams
+.. _Tempest external plugin interface: https://specs.openstack.org/openstack/qa-specs/specs/tempest/implemented/tempest-external-plugin-interface.html
+.. _plugins: https://docs.openstack.org/tempest/latest/plugin.html
+
 
 Scope of these tests
 --------------------
diff --git a/doc/source/framework/overview.rst b/doc/source/framework/overview.rst
index d862770..4902f7b 100644
--- a/doc/source/framework/overview.rst
+++ b/doc/source/framework/overview.rst
@@ -26,9 +26,11 @@
    "Inconsistent" (or failing) cases include:
 
    * Expected result is ``False`` and the test passes. This results in an
-     ``RbacOverPermission`` exception getting thrown.
+     :class:`~rbac_exceptions.RbacOverPermissionException` exception
+     getting thrown.
    * Expected result is ``True`` and the test fails. This results in a
-     ``Forbidden`` exception getting thrown.
+     :class:`~rbac_exceptions.RbacOverPermissionException` exception
+     getting thrown.
 
    For example, a 200 from the API call and a ``False`` result from
    ``oslo.policy`` or a 403 from the API call and a ``True`` result from
diff --git a/doc/source/framework/rbac_utils.rst b/doc/source/framework/rbac_utils.rst
index 69ba045..7143928 100644
--- a/doc/source/framework/rbac_utils.rst
+++ b/doc/source/framework/rbac_utils.rst
@@ -23,8 +23,19 @@
 and test execution, respectively. This is especially true when considering
 custom policy rule definitions, which can be arbitrarily complex.
 
-Patrole, therefore, implicitly splits up each test into 3 stages: set up,
-test execution, and teardown.
+.. _role-overriding:
+
+Role Overriding
+^^^^^^^^^^^^^^^
+
+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.
+
+Patrole implicitly splits up each test into 3 stages: set up, test execution,
+and teardown.
 
 The role workflow is as follows:
 
@@ -43,7 +54,7 @@
 Test Setup
 ----------
 
-Automatic role switch in background.
+Automatic role override in background.
 
 Resources can be set up inside the ``resource_setup`` class method that Tempest
 provides. These resources are typically reserved for "expensive" resources
@@ -59,7 +70,7 @@
 Test Execution
 --------------
 
-Manual role switch required.
+Manual role override required.
 
 "Test execution" here means calling the API endpoint that enforces the policy
 action expected by the ``rbac_rule_validation`` decorator. Test execution
@@ -152,7 +163,7 @@
 Test Cleanup
 ------------
 
-Automatic role switch in background.
+Automatic role override in background.
 
 After the test -- no matter whether it ended successfully or in failure --
 the credentials are overridden with the admin role by the Patrole framework,
diff --git a/doc/source/index.rst b/doc/source/index.rst
index 8368262..255fd9a 100644
--- a/doc/source/index.rst
+++ b/doc/source/index.rst
@@ -2,6 +2,14 @@
 Patrole: Tempest Plugin for RBAC Testing
 ========================================
 
+Overview
+========
+
+.. toctree::
+   :maxdepth: 2
+
+   overview
+
 User's Guide
 ============
 
@@ -12,7 +20,6 @@
    :maxdepth: 2
 
    configuration
-   usage
 
 Patrole Installation Guide
 --------------------------
diff --git a/doc/source/overview.rst b/doc/source/overview.rst
new file mode 100644
index 0000000..795359e
--- /dev/null
+++ b/doc/source/overview.rst
@@ -0,0 +1,178 @@
+========================
+Team and repository tags
+========================
+
+.. image:: https://governance.openstack.org/tc/badges/patrole.svg
+    :target: https://governance.openstack.org/tc/reference/tags/index.html
+
+Patrole - The OpenStack RBAC Validation Test Suite
+==================================================
+
+The documentation for Patrole is officially hosted at:
+https://docs.openstack.org/patrole/latest/
+
+This is a set of integration tests to be run against a live OpenStack
+cluster. Patrole has a battery of tests dedicated to validating the correctness
+and security of the cloud's RBAC implementation.
+
+Design Principles
+-----------------
+
+As a `Tempest plugin`_, Patrole borrows some `design principles`_ from Tempest,
+but not all, as its testing scope is confined to policies.
+
+* Patrole uses OpenStack public interfaces. Tests in Patrole should only touch
+  public OpenStack APIs.
+* Patrole tests should be atomic: they should test policies in isolation.
+  Unlike Tempest, a Patrole test strives to only call a single endpoint at a
+  time. This is because it is important to validate each policy is authorized
+  correctly and the best way to do that is to validate the policy alone.
+* Patrole should validate all policy in code defaults. For testing, Patrole
+  uses the API-to-policy mapping contained in each project's `policy in code`_
+  documentation where applicable.
+
+  For example, Nova's policy in code documentation is located in the
+  `Nova repository`_ under ``nova/policies``. Likewise, Keystone's policy in
+  code documentation is located in the `Keystone repository`_ under
+  ``keystone/common/policies``. The other OpenStack services follow the same
+  directory layout pattern with respect to policy in code.
+
+  .. note::
+
+    Realistically this is not always possible because some services have
+    not yet moved to policy in code.
+
+* Patrole should attempt to clean up after itself; whenever possible it should
+  tear down resources when done.
+
+  .. note::
+
+    Patrole modifies roles dynamically in the background, which affects
+    pre-provisioned credentials. Work is currently underway to clean up
+    modifications made to pre-provisioned credentials.
+
+* Patrole should be self-testing.
+
+.. _Tempest plugin: https://docs.openstack.org/tempest/latest/plugin.html
+.. _design principles: https://docs.openstack.org/tempest/latest/overview.html#design-principles
+.. _policy in code: https://specs.openstack.org/openstack/oslo-specs/specs/newton/policy-in-code.html
+.. _Nova repository: https://github.com/openstack/nova/tree/master/nova/policies
+.. _Keystone repository: https://github.com/openstack/keystone/tree/master/keystone/common/policies
+
+Quickstart
+----------
+
+To run Patrole, you must first have `Tempest`_ installed and configured
+properly. Please reference Tempest's `Quickstart`_ guide to do so. Follow all
+the steps outlined therein. Afterward, proceed with the steps below.
+
+#. You first need to install Patrole. This is done with pip after you check out
+   the Patrole repo::
+
+    $ git clone https://git.openstack.org/openstack/patrole
+    $ pip install patrole/
+
+   This can be done within a venv.
+
+   .. note::
+
+     You may also install Patrole from source code by running::
+
+       pip install -e patrole/
+
+#. Next you must properly configure Patrole, which is relatively
+   straightforward. For details on configuring Patrole refer to the
+   :ref:`patrole-configuration`.
+
+#. Once the configuration is done you're now ready to run Patrole. This can
+   be done using the `tempest_run`_ command. This can be done by running::
+
+     $ tempest run --regex '^patrole_tempest_plugin\.tests\.api'
+
+   There is also the option to use testr directly, or any `testr`_ based test
+   runner, like `ostestr`_. For example, from the workspace dir run::
+
+     $ stestr --regex '(?!.*\[.*\bslow\b.*\])(^patrole_tempest_plugin\.tests\.api))'
+
+   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
+   **Tempest** directory and run::
+
+     $ tox -eall-plugin -- patrole_tempest_plugin.tests.api
+
+   .. note::
+
+     It is possible to run Patrole via ``tox -eall`` in order to run Patrole
+     isolated from other plugins. This can be accomplished by including the
+     installation of services that currently use policy in code -- for example,
+     Nova and Keystone. For example::
+
+       $ tox -evenv-tempest -- pip install /opt/stack/patrole /opt/stack/keystone /opt/stack/nova
+       $ tox -eall -- patrole_tempest_plugin.tests.api
+
+#. Log information from tests is captured in ``tempest.log`` under the Tempest
+   repository. Some Patrole debugging information is captured in that log
+   related to expected test results and :ref:`role-overriding`.
+
+   More detailed RBAC testing log output is emitted to ``patrole.log`` under
+   the Patrole repository. To configure Patrole's logging, see the
+   :ref:`patrole-configuration` guide.
+
+.. _Tempest: https://github.com/openstack/tempest
+.. _Quickstart: https://docs.openstack.org/tempest/latest/overview.html#quickstart
+.. _tempest_run: https://docs.openstack.org/tempest/latest/run.html
+.. _testr: https://testrepository.readthedocs.org/en/latest/MANUAL.html
+.. _ostestr: https://docs.openstack.org/os-testr/latest/
+.. _tox: https://tox.readthedocs.io/en/latest/
+
+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: ::
+
+    [patrole]
+    rbac_test_role = member
+    ...
+
+.. note::
+
+  The ``rbac_test_role`` 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.
+
+For more information about the member role and its nomenclature,
+please see: `<https://ask.openstack.org/en/question/4759/member-vs-_member_/>`__.
+
+Unit Tests
+----------
+
+Patrole also has a set of unit tests which test the Patrole code itself. These
+tests can be run by specifying the test discovery path::
+
+  $ stestr --test-path ./patrole_tempest_plugin/tests/unit run
+
+By setting ``--test-path`` option to ``./patrole_tempest_plugin/tests/unit``
+it specifies that test discovery should only be run on the unit test directory.
+
+Alternatively, there are the py27 and py35 tox jobs which will run the unit
+tests with the corresponding version of Python.
+
+One common activity is to just run a single test; you can do this with tox
+simply by specifying to just run py27 or py35 tests against a single test::
+
+  $ tox -e py27 -- -n patrole_tempest_plugin.tests.unit.test_rbac_utils.RBACUtilsTest.test_override_role_with_missing_admin_role
+
+Or all tests in the test_rbac_utils.py file::
+
+  $ tox -e py27 -- -n patrole_tempest_plugin.tests.unit.test_rbac_utils
+
+You may also use regular expressions to run any matching tests::
+
+  $ tox -e py27 -- test_rbac_utils
+
+For more information on these options and details about stestr, please see the
+`stestr documentation <http://stestr.readthedocs.io/en/latest/MANUAL.html>`_.
diff --git a/doc/source/usage.rst b/doc/source/usage.rst
deleted file mode 100644
index 14c2cc7..0000000
--- a/doc/source/usage.rst
+++ /dev/null
@@ -1,61 +0,0 @@
-.. _patrole-usage:
-
-========
-Usage
-========
-
-Patrole (API) Tests
-===================
-
-If Patrole is installed correctly, then the RBAC tests can be executed
-from inside the tempest root directory as follows::
-
-    $ tox -eall-plugin -- patrole_tempest_plugin.tests.api
-
-To execute patrole tests for a specific module, run::
-
-    $ tox -eall-plugin -- patrole_tempest_plugin.tests.api.compute
-
-.. note::
-
-    It is possible to run Patrole via ``tox -eall`` in order to run Patrole
-    isolated from other plugins. This can be accomplished by including the
-    installation of services that currently use policy in code -- for example,
-    Nova and Keystone. For example::
-
-        $ tox -evenv-tempest -- pip install /opt/stack/patrole /opt/stack/keystone /opt/stack/nova
-        $ tox -eall -- patrole_tempest_plugin.tests.api
-..
-
-To change the role that the patrole tests are being run as, edit
-``rbac_test_role`` in the ``patrole`` section of tempest.conf: ::
-
-    [patrole]
-    rbac_test_role = Member
-    ...
-
-.. note::
-
-    The ``rbac_test_role`` 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, some services, like Heat, take
-    advantage of a role called **heat_stack_user**, as it appears frequently
-    in Heat's policy.json.
-
-For more information about the Member role,
-please see: `<https://ask.openstack.org/en/question/4759/member-vs-_member_/>`__.
-
-Unit Tests
-==========
-
-Patrole includes unit tests for its RBAC framework. They can be run by
-executing::
-
-    $ tox -e py27
-
-or::
-
-    $ tox -e py35
-
-against the Python 3.5 interpreter.
diff --git a/etc/patrole.conf.sample b/etc/patrole.conf.sample
index 8e7931b..6de073d 100644
--- a/etc/patrole.conf.sample
+++ b/etc/patrole.conf.sample
@@ -45,7 +45,7 @@
 #
 # YAML definition: allowed
 # test run: not allowed
-# test result: fail (under-permission, e.g. Forbidden exception)
+# test result: fail (under-permission)
 #
 # YAML definition: not allowed
 # test run: allowed
diff --git a/patrole_tempest_plugin/config.py b/patrole_tempest_plugin/config.py
index ee7a6c5..4dc27b9 100644
--- a/patrole_tempest_plugin/config.py
+++ b/patrole_tempest_plugin/config.py
@@ -55,7 +55,7 @@
 
 YAML definition: allowed
 test run: not allowed
-test result: fail (under-permission, e.g. Forbidden exception)
+test result: fail (under-permission)
 
 YAML definition: not allowed
 test run: allowed
diff --git a/patrole_tempest_plugin/rbac_exceptions.py b/patrole_tempest_plugin/rbac_exceptions.py
index e75b8ec..980672a 100644
--- a/patrole_tempest_plugin/rbac_exceptions.py
+++ b/patrole_tempest_plugin/rbac_exceptions.py
@@ -41,8 +41,27 @@
     message = "RBAC resource setup failed"
 
 
-class RbacOverPermission(exceptions.TempestException):
-    message = "Action performed that should not be permitted"
+class RbacOverPermissionException(exceptions.TempestException):
+    """Raised when the expected result is failure but the actual result is
+    pass.
+    """
+    message = "Unauthorized action was allowed to be performed"
+
+
+class RbacUnderPermissionException(exceptions.TempestException):
+    """Raised when the expected result is pass but the actual result is
+    failure.
+    """
+    message = "Authorized action was not allowed to be performed"
+
+
+class RbacExpectedWrongException(exceptions.TempestException):
+    """Raised when the expected exception does not match the actual exception
+    raised, when both are instances of Forbidden or NotFound, indicating
+    the test provides a wrong argument to `expected_error_codes`.
+    """
+    message = ("Expected %(expected)s to be raised but %(actual)s was raised "
+               "instead. Actual exception: %(exception)s")
 
 
 class RbacInvalidService(exceptions.TempestException):
diff --git a/patrole_tempest_plugin/rbac_rule_validation.py b/patrole_tempest_plugin/rbac_rule_validation.py
index fa87158..2f6759d 100644
--- a/patrole_tempest_plugin/rbac_rule_validation.py
+++ b/patrole_tempest_plugin/rbac_rule_validation.py
@@ -64,9 +64,9 @@
 
     1) If *expected* is True and the test passes (*actual*), this is a success.
     2) If *expected* is True and the test fails (*actual*), this results in a
-       `Forbidden` exception failure.
+       ``RbacUnderPermissionException`` exception failure.
     3) If *expected* is False and the test passes (*actual*), this results in
-       an `OverPermission` exception failure.
+       an ``RbacOverPermissionException`` exception failure.
     4) If *expected* is False and the test fails (*actual*), this is a success.
 
     As such, negative and positive testing can be applied using this decorator.
@@ -124,8 +124,10 @@
             })
 
     :raises NotFound: If ``service`` is invalid.
-    :raises Forbidden: For item (2) above.
-    :raises RbacOverPermission: For item (3) above.
+    :raises RbacUnderPermissionException: For item (2) above.
+    :raises RbacOverPermissionException: For item (3) above.
+    :raises RbacExpectedWrongException: When a 403 is expected but a 404
+        is raised instead or vice versa.
 
     Examples::
 
@@ -205,17 +207,28 @@
                                sorted(set(rules) - set(disallowed_rules)),
                                sorted(disallowed_rules)))
                     LOG.error(msg)
-                    raise exceptions.Forbidden(
+                    raise rbac_exceptions.RbacUnderPermissionException(
                         "%s Exception was: %s" % (msg, e))
-            except Exception as e:
-                with excutils.save_and_reraise_exception():
-                    exc_info = sys.exc_info()
-                    error_details = six.text_type(exc_info[1])
-                    msg = ("An unexpected exception has occurred during test: "
-                           "%s. Exception was: %s" % (test_func.__name__,
-                                                      error_details))
-                    test_status = 'Error, %s' % (error_details)
-                    LOG.error(msg)
+            except Exception as actual_exception:
+                if _check_for_expected_mismatch_exception(expected_exception,
+                                                          actual_exception):
+                    LOG.error('Expected and actual exceptions do not match. '
+                              'Expected: %s. Actual: %s.',
+                              expected_exception,
+                              actual_exception.__class__)
+                    raise rbac_exceptions.RbacExpectedWrongException(
+                        expected=expected_exception,
+                        actual=actual_exception.__class__,
+                        exception=actual_exception)
+                else:
+                    with excutils.save_and_reraise_exception():
+                        exc_info = sys.exc_info()
+                        error_details = six.text_type(exc_info[1])
+                        msg = ("An unexpected exception has occurred during "
+                               "test: %s. Exception was: %s" % (
+                                   test_func.__name__, error_details))
+                        test_status = 'Error, %s' % (error_details)
+                        LOG.error(msg)
             else:
                 if not allowed:
                     msg = (
@@ -225,7 +238,7 @@
                         )
                     )
                     LOG.error(msg)
-                    raise rbac_exceptions.RbacOverPermission(msg)
+                    raise rbac_exceptions.RbacOverPermissionException(msg)
             finally:
                 if CONF.patrole_log.enable_reporting:
                     RBACLOG.info(
@@ -240,7 +253,6 @@
 
 
 def _prepare_multi_policy(rule, rules, exp_error_code, exp_error_codes):
-
     if exp_error_codes:
         if not rules:
             msg = ("The `rules` list must be provided if using the "
@@ -415,3 +427,12 @@
         formatted_target_data[user_attribute] = attr_value
 
     return formatted_target_data
+
+
+def _check_for_expected_mismatch_exception(expected_exception,
+                                           actual_exception):
+    permission_exceptions = (exceptions.Forbidden, exceptions.NotFound)
+    if isinstance(actual_exception, permission_exceptions):
+        if not isinstance(actual_exception, expected_exception.__class__):
+            return True
+    return False
diff --git a/patrole_tempest_plugin/services/__init__.py b/patrole_tempest_plugin/services/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/patrole_tempest_plugin/services/__init__.py
+++ /dev/null
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 13faca1..d97f382 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
@@ -610,9 +610,6 @@
         network_id = self.network['id']
         interface = self.interfaces_client.create_interface(
             self.server['id'], net_id=network_id)['interfaceAttachment']
-        waiters.wait_for_interface_status(
-            self.interfaces_client, self.server['id'],
-            interface['port_id'], 'ACTIVE')
         self.addCleanup(
             waiters.wait_for_interface_detach, self.interfaces_client,
             self.server['id'], interface['port_id'])
@@ -620,6 +617,9 @@
             test_utils.call_and_ignore_notfound_exc,
             self.interfaces_client.delete_interface,
             self.server['id'], interface['port_id'])
+        waiters.wait_for_interface_status(
+            self.interfaces_client, self.server['id'],
+            interface['port_id'], 'ACTIVE')
         return interface
 
     @testtools.skipUnless(CONF.compute_feature_enabled.interface_attach,
@@ -661,9 +661,6 @@
         with self.rbac_utils.override_role(self):
             interface = self.interfaces_client.create_interface(
                 self.server['id'], net_id=network_id)['interfaceAttachment']
-        waiters.wait_for_interface_status(
-            self.interfaces_client, self.server['id'],
-            interface['port_id'], 'ACTIVE')
         self.addCleanup(
             waiters.wait_for_interface_detach, self.interfaces_client,
             self.server['id'], interface['port_id'])
@@ -671,6 +668,9 @@
             test_utils.call_and_ignore_notfound_exc,
             self.interfaces_client.delete_interface,
             self.server['id'], interface['port_id'])
+        waiters.wait_for_interface_status(
+            self.interfaces_client, self.server['id'],
+            interface['port_id'], 'ACTIVE')
 
     @testtools.skipUnless(CONF.compute_feature_enabled.interface_attach,
                           "Interface attachment is not available.")
@@ -740,3 +740,17 @@
         with self.rbac_utils.override_role(self):
             self.servers_client.add_fixed_ip(self.server['id'],
                                              networkId=network_id)
+        # Get the Fixed IP from server.
+        server_detail = self.servers_client.show_server(
+            self.server['id'])['server']
+        fixed_ip = None
+        for ip_set in server_detail['addresses']:
+            for ip in server_detail['addresses'][ip_set]:
+                if ip['OS-EXT-IPS:type'] == 'fixed':
+                    fixed_ip = ip['addr']
+                    break
+            if fixed_ip is not None:
+                break
+        # Remove the fixed IP from server.
+        self.servers_client.remove_fixed_ip(self.server['id'],
+                                            address=fixed_ip)
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 6b03ebe..2756a10 100644
--- a/patrole_tempest_plugin/tests/api/network/test_agents_rbac.py
+++ b/patrole_tempest_plugin/tests/api/network/test_agents_rbac.py
@@ -50,8 +50,8 @@
 
     @decorators.idempotent_id('8ca68fdb-eaf6-4880-af82-ba0982949dec')
     @rbac_rule_validation.action(service="neutron",
-                                 rule="update_agent",
-                                 expected_error_code=404)
+                                 rules=["get_agent", "update_agent"],
+                                 expected_error_codes=[404, 403])
     def test_update_agent(self):
         """Update agent test.
 
diff --git a/patrole_tempest_plugin/tests/api/network/test_floating_ips_rbac.py b/patrole_tempest_plugin/tests/api/network/test_floating_ips_rbac.py
index 20e4aa7..ed52c34 100644
--- a/patrole_tempest_plugin/tests/api/network/test_floating_ips_rbac.py
+++ b/patrole_tempest_plugin/tests/api/network/test_floating_ips_rbac.py
@@ -73,8 +73,11 @@
         with self.rbac_utils.override_role(self):
             self._create_floatingip()
 
-    @rbac_rule_validation.action(service="neutron",
-                                 rule="create_floatingip:floating_ip_address")
+    @rbac_rule_validation.action(
+        service="neutron",
+        rules=["create_floatingip",
+               "create_floatingip:floating_ip_address"],
+        expected_error_codes=[403, 403])
     @decorators.idempotent_id('a8bb826a-403d-4130-a55d-120a0a660806')
     def test_create_floating_ip_floatingip_address(self):
         """Create floating IP with address.
@@ -87,7 +90,8 @@
             self._create_floatingip(floating_ip_address=fip)
 
     @rbac_rule_validation.action(service="neutron",
-                                 rule="update_floatingip")
+                                 rules=["get_floatingip", "update_floatingip"],
+                                 expected_error_codes=[404, 403])
     @decorators.idempotent_id('2ab1b060-19f8-4ef6-a838-e2ab7b377c63')
     def test_update_floating_ip(self):
         """Update floating IP.
@@ -115,8 +119,8 @@
             self.floating_ips_client.show_floatingip(floating_ip['id'])
 
     @rbac_rule_validation.action(service="neutron",
-                                 rule="delete_floatingip",
-                                 expected_error_code=404)
+                                 rules=["get_floatingip", "delete_floatingip"],
+                                 expected_error_codes=[404, 403])
     @decorators.idempotent_id('2611b068-30d4-4241-a78f-1b801a14db7e')
     def test_delete_floating_ip(self):
         """Delete floating IP.
diff --git a/patrole_tempest_plugin/tests/api/network/test_metering_label_rules_rbac.py b/patrole_tempest_plugin/tests/api/network/test_metering_label_rules_rbac.py
index 7a9d814..adab1e6 100644
--- a/patrole_tempest_plugin/tests/api/network/test_metering_label_rules_rbac.py
+++ b/patrole_tempest_plugin/tests/api/network/test_metering_label_rules_rbac.py
@@ -88,8 +88,9 @@
                 label_rule['id'])
 
     @rbac_rule_validation.action(service="neutron",
-                                 rule="delete_metering_label_rule",
-                                 expected_error_code=404)
+                                 rules=["get_metering_label_rule",
+                                        "delete_metering_label_rule"],
+                                 expected_error_codes=[404, 403])
     @decorators.idempotent_id('e3adc88c-05c0-43a7-8e32-63947ae4890e')
     def test_delete_metering_label_rule(self):
         """Delete metering label rule.
diff --git a/patrole_tempest_plugin/tests/api/network/test_metering_labels_rbac.py b/patrole_tempest_plugin/tests/api/network/test_metering_labels_rbac.py
index abd7326..0231868 100644
--- a/patrole_tempest_plugin/tests/api/network/test_metering_labels_rbac.py
+++ b/patrole_tempest_plugin/tests/api/network/test_metering_labels_rbac.py
@@ -71,8 +71,9 @@
             self.metering_labels_client.show_metering_label(label['id'])
 
     @rbac_rule_validation.action(service="neutron",
-                                 rule="delete_metering_label",
-                                 expected_error_code=404)
+                                 rules=["get_metering_label",
+                                        "delete_metering_label"],
+                                 expected_error_codes=[404, 403])
     @decorators.idempotent_id('1621ccfe-2e3f-4d16-98aa-b620f9d00404')
     def test_delete_metering_label(self):
         """Delete metering label.
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 1dee46b..0097c7b 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
@@ -66,7 +66,9 @@
         return network
 
     @rbac_rule_validation.action(service="neutron",
-                                 rule="create_network:segments")
+                                 rules=["create_network",
+                                        "create_network:segments"],
+                                 expected_error_codes=[403, 403])
     @decorators.idempotent_id('9e1d0c3d-92e3-40e3-855e-bfbb72ea6e0b')
     def test_create_network_segments(self):
         """Create network with segments.
@@ -77,7 +79,9 @@
             self._create_network_segments()
 
     @rbac_rule_validation.action(service="neutron",
-                                 rule="update_network:segments")
+                                 rules=["get_network", "update_network",
+                                        "update_network:segments"],
+                                 expected_error_codes=[404, 403, 403])
     @decorators.idempotent_id('0f45232a-7b59-4bb1-9a91-db77d0a8cc9b')
     def test_update_network_segments(self):
         """Update network segments.
@@ -92,7 +96,9 @@
                                                 segments=new_segments)
 
     @rbac_rule_validation.action(service="neutron",
-                                 rule="get_network:segments")
+                                 rules=["get_network",
+                                        "get_network:segments"],
+                                 expected_error_codes=[404, 403])
     @decorators.idempotent_id('094ff9b7-0c3b-4515-b19b-b9d2031337bd')
     def test_show_network_segments(self):
         """Show network segments.
@@ -113,4 +119,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(True)
+            raise rbac_exceptions.RbacMalformedResponse(empty=True)
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 932683d..1a0e186 100644
--- a/patrole_tempest_plugin/tests/api/network/test_networks_rbac.py
+++ b/patrole_tempest_plugin/tests/api/network/test_networks_rbac.py
@@ -99,7 +99,9 @@
             self._create_network()
 
     @rbac_rule_validation.action(service="neutron",
-                                 rule="create_network:shared")
+                                 rules=["create_network",
+                                        "create_network:shared"],
+                                 expected_error_codes=[403, 403])
     @decorators.idempotent_id('ccabf2a9-28c8-44b2-80e6-ffd65d43eef2')
     def test_create_network_shared(self):
 
@@ -112,7 +114,9 @@
 
     @utils.requires_ext(extension='external-net', service='network')
     @rbac_rule_validation.action(service="neutron",
-                                 rule="create_network:router:external")
+                                 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):
 
@@ -124,8 +128,11 @@
             self._create_network(router_external=True)
 
     @utils.requires_ext(extension='provider', service='network')
-    @rbac_rule_validation.action(service="neutron",
-                                 rule="create_network:provider:network_type")
+    @rbac_rule_validation.action(
+        service="neutron",
+        rules=["create_network",
+               "create_network:provider:network_type"],
+        expected_error_codes=[403, 403])
     @decorators.idempotent_id('3c42f7b8-b80c-44ef-8fa4-69ec4b1836bc')
     def test_create_network_provider_network_type(self):
 
@@ -139,7 +146,9 @@
     @utils.requires_ext(extension='provider', service='network')
     @rbac_rule_validation.action(
         service="neutron",
-        rule="create_network:provider:segmentation_id")
+        rules=["create_network",
+               "create_network:provider:segmentation_id"],
+        expected_error_codes=[403, 403])
     @decorators.idempotent_id('b9decb7b-68ef-4504-b99b-41edbf7d2af5')
     def test_create_network_provider_segmentation_id(self):
 
@@ -152,7 +161,8 @@
                                  provider_segmentation_id=200)
 
     @rbac_rule_validation.action(service="neutron",
-                                 rule="update_network")
+                                 rules=["get_network", "update_network"],
+                                 expected_error_codes=[404, 403])
     @decorators.idempotent_id('6485bb4e-e110-48ae-83e1-3ec8b40c3107')
     def test_update_network(self):
 
@@ -167,7 +177,10 @@
             self._update_network(name=updated_name)
 
     @rbac_rule_validation.action(service="neutron",
-                                 rule="update_network:shared")
+                                 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):
 
@@ -181,7 +194,10 @@
 
     @utils.requires_ext(extension='external-net', service='network')
     @rbac_rule_validation.action(service="neutron",
-                                 rule="update_network:router:external")
+                                 rules=["get_network",
+                                        "update_network",
+                                        "update_network:router:external"],
+                                 expected_error_codes=[404, 403, 403])
     @decorators.idempotent_id('34884c22-499b-4960-97f1-e2ed8522a9c9')
     def test_update_network_router_external(self):
 
@@ -194,7 +210,8 @@
             self._update_network(net_id=network['id'], router_external=True)
 
     @rbac_rule_validation.action(service="neutron",
-                                 rule="get_network")
+                                 rule="get_network",
+                                 expected_error_code=404)
     @decorators.idempotent_id('0eb62d04-338a-4ff4-a8fa-534e52110534')
     def test_show_network(self):
 
@@ -207,7 +224,9 @@
 
     @utils.requires_ext(extension='external-net', service='network')
     @rbac_rule_validation.action(service="neutron",
-                                 rule="get_network:router:external")
+                                 rules=["get_network",
+                                        "get_network:router:external"],
+                                 expected_error_codes=[404, 403])
     @decorators.idempotent_id('529e4814-22e9-413f-af48-8fefcd637344')
     def test_show_network_router_external(self):
 
@@ -218,12 +237,17 @@
         kwargs = {'fields': 'router:external'}
 
         with self.rbac_utils.override_role(self):
-            self.networks_client.show_network(self.network['id'],
-                                              **kwargs)
+            retrieved_network = self.networks_client.show_network(
+                self.network['id'], **kwargs)['network']
+
+        if len(retrieved_network) == 0:
+            raise rbac_exceptions.RbacMalformedResponse(empty=True)
 
     @utils.requires_ext(extension='provider', service='network')
     @rbac_rule_validation.action(service="neutron",
-                                 rule="get_network:provider:network_type")
+                                 rules=["get_network",
+                                        "get_network:provider:network_type"],
+                                 expected_error_codes=[404, 403])
     @decorators.idempotent_id('6521dd60-0950-458b-8491-09d3c84ac0f4')
     def test_show_network_provider_network_type(self):
 
@@ -238,11 +262,14 @@
                 self.network['id'], **kwargs)['network']
 
         if len(retrieved_network) == 0:
-            raise rbac_exceptions.RbacMalformedResponse(True)
+            raise rbac_exceptions.RbacMalformedResponse(empty=True)
 
     @utils.requires_ext(extension='provider', service='network')
-    @rbac_rule_validation.action(service="neutron",
-                                 rule="get_network:provider:physical_network")
+    @rbac_rule_validation.action(
+        service="neutron",
+        rules=["get_network",
+               "get_network:provider:physical_network"],
+        expected_error_codes=[404, 403])
     @decorators.idempotent_id('c049f11a-240c-4a85-ad43-a4d3fd0a5e39')
     def test_show_network_provider_physical_network(self):
 
@@ -260,8 +287,11 @@
             raise rbac_exceptions.RbacMalformedResponse(empty=True)
 
     @utils.requires_ext(extension='provider', service='network')
-    @rbac_rule_validation.action(service="neutron",
-                                 rule="get_network:provider:segmentation_id")
+    @rbac_rule_validation.action(
+        service="neutron",
+        rules=["get_network",
+               "get_network:provider:segmentation_id"],
+        expected_error_codes=[404, 403])
     @decorators.idempotent_id('38d9f085-6365-4f81-bac9-c53c294d727e')
     def test_show_network_provider_segmentation_id(self):
 
@@ -278,11 +308,9 @@
         if len(retrieved_network) == 0:
             raise rbac_exceptions.RbacMalformedResponse(empty=True)
 
-        key = retrieved_network.get('provider:segmentation_id', "NotFound")
-        self.assertNotEqual(key, "NotFound")
-
     @rbac_rule_validation.action(service="neutron",
-                                 rule="delete_network")
+                                 rules=["get_network", "delete_network"],
+                                 expected_error_codes=[404, 403])
     @decorators.idempotent_id('56ca50ed-ac58-49d6-b239-ed39e7124d5c')
     def test_delete_network(self):
 
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 a8c7d68..2cf3cd6 100644
--- a/patrole_tempest_plugin/tests/api/network/test_ports_rbac.py
+++ b/patrole_tempest_plugin/tests/api/network/test_ports_rbac.py
@@ -68,7 +68,9 @@
 
     @decorators.idempotent_id('045ee797-4962-4913-b96a-5d7ea04099e7')
     @rbac_rule_validation.action(service="neutron",
-                                 rule="create_port:device_owner")
+                                 rules=["create_port",
+                                        "create_port:device_owner"],
+                                 expected_error_codes=[403, 403])
     def test_create_port_device_owner(self):
         with self.rbac_utils.override_role(self):
             self.create_port(self.network,
@@ -76,14 +78,18 @@
 
     @decorators.idempotent_id('c4fa8844-f5ef-4daa-bfa2-b89897dfaedf')
     @rbac_rule_validation.action(service="neutron",
-                                 rule="create_port:port_security_enabled")
+                                 rules=["create_port",
+                                        "create_port:port_security_enabled"],
+                                 expected_error_codes=[403, 403])
     def test_create_port_security_enabled(self):
         with self.rbac_utils.override_role(self):
             self.create_port(self.network, port_security_enabled=True)
 
     @utils.requires_ext(extension='binding', service='network')
     @rbac_rule_validation.action(service="neutron",
-                                 rule="create_port:binding:host_id")
+                                 rules=["create_port",
+                                        "create_port:binding:host_id"],
+                                 expected_error_codes=[403, 403])
     @decorators.idempotent_id('a54bd6b8-a7eb-4101-bfe8-093930b0d660')
     def test_create_port_binding_host_id(self):
 
@@ -95,7 +101,9 @@
 
     @utils.requires_ext(extension='binding', service='network')
     @rbac_rule_validation.action(service="neutron",
-                                 rule="create_port:binding:profile")
+                                 rules=["create_port",
+                                        "create_port:binding:profile"],
+                                 expected_error_codes=[403, 403])
     @decorators.idempotent_id('98fa38ab-c2ed-46a0-99f0-59f18cbd257a')
     def test_create_port_binding_profile(self):
 
@@ -111,7 +119,9 @@
         CONF.policy_feature_enabled.create_port_fixed_ips_ip_address_policy,
         '"create_port:fixed_ips:ip_address" must be available in the cloud.')
     @rbac_rule_validation.action(service="neutron",
-                                 rule="create_port:fixed_ips:ip_address")
+                                 rules=["create_port",
+                                        "create_port:fixed_ips:ip_address"],
+                                 expected_error_codes=[403, 403])
     @decorators.idempotent_id('2551e10d-006a-413c-925a-8c6f834c09ac')
     def test_create_port_fixed_ips_ip_address(self):
 
@@ -126,7 +136,9 @@
             self.create_port(**post_body)
 
     @rbac_rule_validation.action(service="neutron",
-                                 rule="create_port:mac_address")
+                                 rules=["create_port",
+                                        "create_port:mac_address"],
+                                 expected_error_codes=[403, 403])
     @decorators.idempotent_id('aee6d0be-a7f3-452f-aefc-796b4eb9c9a8')
     def test_create_port_mac_address(self):
 
@@ -137,7 +149,9 @@
             self.create_port(**post_body)
 
     @rbac_rule_validation.action(service="neutron",
-                                 rule="create_port:allowed_address_pairs")
+                                 rules=["create_port",
+                                        "create_port:allowed_address_pairs"],
+                                 expected_error_codes=[403, 403])
     @decorators.idempotent_id('b638d1f4-d903-4ca8-aa2a-6fd603c5ec3a')
     def test_create_port_allowed_address_pairs(self):
 
@@ -161,7 +175,9 @@
 
     @utils.requires_ext(extension='binding', service='network')
     @rbac_rule_validation.action(service="neutron",
-                                 rule="get_port:binding:vif_type")
+                                 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):
 
@@ -179,7 +195,9 @@
 
     @utils.requires_ext(extension='binding', service='network')
     @rbac_rule_validation.action(service="neutron",
-                                 rule="get_port:binding:vif_details")
+                                 rules=["get_port",
+                                        "get_port:binding:vif_details"],
+                                 expected_error_codes=[404, 403])
     @decorators.idempotent_id('e42bfd77-fcce-45ee-9728-3424300f0d6f')
     def test_show_port_binding_vif_details(self):
 
@@ -197,7 +215,9 @@
 
     @utils.requires_ext(extension='binding', service='network')
     @rbac_rule_validation.action(service="neutron",
-                                 rule="get_port:binding:host_id")
+                                 rules=["get_port",
+                                        "get_port:binding:host_id"],
+                                 expected_error_codes=[404, 403])
     @decorators.idempotent_id('8e61bcdc-6f81-443c-833e-44410266551e')
     def test_show_port_binding_host_id(self):
 
@@ -218,7 +238,9 @@
 
     @utils.requires_ext(extension='binding', service='network')
     @rbac_rule_validation.action(service="neutron",
-                                 rule="get_port:binding:profile")
+                                 rules=["get_port",
+                                        "get_port:binding:profile"],
+                                 expected_error_codes=[404, 403])
     @decorators.idempotent_id('d497cea9-c4ad-42e0-acc9-8d257d6b01fc')
     def test_show_port_binding_profile(self):
 
@@ -239,7 +261,8 @@
                 attribute='binding:profile')
 
     @rbac_rule_validation.action(service="neutron",
-                                 rule="update_port")
+                                 rules=["get_port", "update_port"],
+                                 expected_error_codes=[404, 403])
     @decorators.idempotent_id('afa80981-3c59-42fd-9531-3bcb2cd03711')
     def test_update_port(self):
         with self.rbac_utils.override_role(self):
@@ -250,7 +273,9 @@
 
     @decorators.idempotent_id('08d70f59-67cb-4fb1-bd6c-a5e59dd5db2b')
     @rbac_rule_validation.action(service="neutron",
-                                 rule="update_port:device_owner")
+                                 rules=["get_port", "update_port",
+                                        "update_port:device_owner"],
+                                 expected_error_codes=[404, 403, 403])
     def test_update_port_device_owner(self):
         original_device_owner = self.port['device_owner']
 
@@ -261,7 +286,9 @@
                         device_owner=original_device_owner)
 
     @rbac_rule_validation.action(service="neutron",
-                                 rule="update_port:mac_address")
+                                 rules=["get_port", "update_port",
+                                        "update_port:mac_address"],
+                                 expected_error_codes=[404, 403, 403])
     @decorators.idempotent_id('507140c8-7b14-4d63-b627-2103691d887e')
     def test_update_port_mac_address(self):
         original_mac_address = self.port['mac_address']
@@ -276,7 +303,9 @@
         CONF.policy_feature_enabled.update_port_fixed_ips_ip_address_policy,
         '"update_port:fixed_ips:ip_address" must be available in the cloud.')
     @rbac_rule_validation.action(service="neutron",
-                                 rule="update_port:fixed_ips:ip_address")
+                                 rules=["get_port", "update_port",
+                                        "update_port:fixed_ips:ip_address"],
+                                 expected_error_codes=[404, 403, 403])
     @decorators.idempotent_id('c091c825-532b-4c6f-a14f-affd3259c1c3')
     def test_update_port_fixed_ips_ip_address(self):
 
@@ -291,15 +320,20 @@
             self.ports_client.update_port(port['id'], fixed_ips=fixed_ips)
 
     @rbac_rule_validation.action(service="neutron",
-                                 rule="update_port:port_security_enabled")
+                                 rules=["get_port", "update_port",
+                                        "update_port:port_security_enabled"],
+                                 expected_error_codes=[404, 403, 403])
     @decorators.idempotent_id('795541af-6652-4e35-9581-fd58224f7545')
     def test_update_port_security_enabled(self):
         with self.rbac_utils.override_role(self):
-            self.ports_client.update_port(self.port['id'], security_groups=[])
+            self.ports_client.update_port(self.port['id'],
+                                          port_security_enabled=True)
 
     @utils.requires_ext(extension='binding', service='network')
     @rbac_rule_validation.action(service="neutron",
-                                 rule="update_port:binding:host_id")
+                                 rules=["get_port", "update_port",
+                                        "update_port:binding:host_id"],
+                                 expected_error_codes=[404, 403, 403])
     @decorators.idempotent_id('24206a72-0d90-4712-918c-5c9a1ebef64d')
     def test_update_port_binding_host_id(self):
 
@@ -315,7 +349,9 @@
 
     @utils.requires_ext(extension='binding', service='network')
     @rbac_rule_validation.action(service="neutron",
-                                 rule="update_port:binding:profile")
+                                 rules=["get_port", "update_port",
+                                        "update_port:binding:profile"],
+                                 expected_error_codes=[404, 403, 403])
     @decorators.idempotent_id('990ea8d1-9257-4f71-a3bf-d6d0914625c5')
     def test_update_port_binding_profile(self):
 
@@ -333,7 +369,9 @@
             self.ports_client.update_port(**updated_body)
 
     @rbac_rule_validation.action(service="neutron",
-                                 rule="update_port:allowed_address_pairs")
+                                 rules=["get_port", "update_port",
+                                        "update_port:allowed_address_pairs"],
+                                 expected_error_codes=[404, 403, 403])
     @decorators.idempotent_id('729c2151-bb49-4f4f-9d58-3ed8819b7582')
     def test_update_port_allowed_address_pairs(self):
 
@@ -349,8 +387,8 @@
                                           allowed_address_pairs=address_pairs)
 
     @rbac_rule_validation.action(service="neutron",
-                                 rule="delete_port",
-                                 expected_error_code=404)
+                                 rules=["get_port", "delete_port"],
+                                 expected_error_codes=[404, 403])
     @decorators.idempotent_id('1cf8e582-bc09-46cb-b32a-82bf991ad56f')
     def test_delete_port(self):
 
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 812b0c1..a3d973d 100644
--- a/patrole_tempest_plugin/tests/api/network/test_routers_rbac.py
+++ b/patrole_tempest_plugin/tests/api/network/test_routers_rbac.py
@@ -71,7 +71,9 @@
     @decorators.idempotent_id('6139eb97-95c0-40d8-a109-99de11ab2e5e')
     @utils.requires_ext(extension='l3-ha', service='network')
     @rbac_rule_validation.action(service="neutron",
-                                 rule="create_router:ha")
+                                 rules=["create_router",
+                                        "create_router:ha"],
+                                 expected_error_codes=[403, 403])
     def test_create_high_availability_router(self):
         """Create high-availability router
 
@@ -85,7 +87,9 @@
     @decorators.idempotent_id('c6254ca6-2728-412d-803d-d4aa3935e56d')
     @utils.requires_ext(extension='dvr', service='network')
     @rbac_rule_validation.action(service="neutron",
-                                 rule="create_router:distributed")
+                                 rules=["create_router",
+                                        "create_router:distributed"],
+                                 expected_error_codes=[403, 403])
     def test_create_distributed_router(self):
         """Create distributed router
 
@@ -99,7 +103,9 @@
     @utils.requires_ext(extension='ext-gw-mode', service='network')
     @rbac_rule_validation.action(
         service="neutron",
-        rule="create_router:external_gateway_info:enable_snat")
+        rules=["create_router",
+               "create_router:external_gateway_info:enable_snat"],
+        expected_error_codes=[403, 403])
     @decorators.idempotent_id('3c5acd49-0ec7-4109-ab51-640557b48ebc')
     def test_create_router_enable_snat(self):
         """Create Router Snat
@@ -119,7 +125,9 @@
 
     @rbac_rule_validation.action(
         service="neutron",
-        rule="create_router:external_gateway_info:external_fixed_ips")
+        rules=["create_router",
+               "create_router:external_gateway_info:external_fixed_ips"],
+        expected_error_codes=[403, 403])
     @decorators.idempotent_id('d0354369-a040-4349-b869-645c8aed13cd')
     def test_create_router_external_fixed_ips(self):
         """Create Router Fixed IPs
@@ -158,7 +166,9 @@
     @decorators.idempotent_id('3ed26ea2-b419-410c-b4b5-576c1edafa06')
     @utils.requires_ext(extension='dvr', service='network')
     @rbac_rule_validation.action(service="neutron",
-                                 rule="get_router:distributed")
+                                 rules=["get_router",
+                                        "get_router:distributed"],
+                                 expected_error_codes=[404, 403])
     def test_show_distributed_router(self):
         """Get distributed router
 
@@ -179,7 +189,8 @@
     @decorators.idempotent_id('defc502c-4159-4824-b4d9-3cdcc39015b2')
     @utils.requires_ext(extension='l3-ha', service='network')
     @rbac_rule_validation.action(service="neutron",
-                                 rule="get_router:ha")
+                                 rules=["get_router", "get_router:ha"],
+                                 expected_error_codes=[404, 403])
     def test_show_high_availability_router(self):
         """GET high-availability router
 
@@ -197,8 +208,9 @@
             raise rbac_exceptions.RbacMalformedResponse(
                 attribute='ha')
 
-    @rbac_rule_validation.action(
-        service="neutron", rule="update_router")
+    @rbac_rule_validation.action(service="neutron",
+                                 rules=["get_router", "update_router"],
+                                 expected_error_codes=[404, 403])
     @decorators.idempotent_id('3d182f4e-0023-4218-9aa0-ea2b0ae0bd7a')
     def test_update_router(self):
         """Update Router
@@ -210,8 +222,10 @@
         with self.rbac_utils.override_role(self):
             self.routers_client.update_router(self.router['id'], name=new_name)
 
-    @rbac_rule_validation.action(
-        service="neutron", rule="update_router:external_gateway_info")
+    @rbac_rule_validation.action(service="neutron",
+                                 rules=["get_router", "update_router",
+                                        "update_router:external_gateway_info"],
+                                 expected_error_codes=[404, 403, 403])
     @decorators.idempotent_id('5a6ae104-a9c3-4b56-8622-e1a0a0194474')
     def test_update_router_external_gateway_info(self):
         """Update Router External Gateway Info
@@ -225,7 +239,10 @@
 
     @rbac_rule_validation.action(
         service="neutron",
-        rule="update_router:external_gateway_info:network_id")
+        rules=["get_router", "update_router",
+               "update_router:external_gateway_info",
+               "update_router:external_gateway_info:network_id"],
+        expected_error_codes=[404, 403, 403, 403])
     @decorators.idempotent_id('f1fc5a23-e3d8-44f0-b7bc-47006ad9d3d4')
     def test_update_router_external_gateway_info_network_id(self):
         """Update Router External Gateway Info Network Id
@@ -245,7 +262,10 @@
     @utils.requires_ext(extension='ext-gw-mode', service='network')
     @rbac_rule_validation.action(
         service="neutron",
-        rule="update_router:external_gateway_info:enable_snat")
+        rules=["get_router", "update_router",
+               "update_router:external_gateway_info",
+               "update_router:external_gateway_info:enable_snat"],
+        expected_error_codes=[404, 403, 403, 403])
     @decorators.idempotent_id('515a2954-3d79-4695-aeb9-d1c222765840')
     def test_update_router_enable_snat(self):
         """Update Router External Gateway Info Enable Snat
@@ -265,7 +285,10 @@
 
     @rbac_rule_validation.action(
         service="neutron",
-        rule="update_router:external_gateway_info:external_fixed_ips")
+        rules=["get_router", "update_router",
+               "update_router:external_gateway_info",
+               "update_router:external_gateway_info:external_fixed_ips"],
+        expected_error_codes=[404, 403, 403, 403])
     @decorators.idempotent_id('f429e5ee-8f0a-4667-963e-72dd95d5adee')
     def test_update_router_external_fixed_ips(self):
         """Update Router External Gateway Info External Fixed Ips
@@ -291,7 +314,9 @@
     @decorators.idempotent_id('ddc20731-dea1-4321-9abf-8772bf0b5977')
     @utils.requires_ext(extension='l3-ha', service='network')
     @rbac_rule_validation.action(service="neutron",
-                                 rule="update_router:ha")
+                                 rules=["get_router", "update_router",
+                                        "update_router:ha"],
+                                 expected_error_codes=[404, 403, 403])
     def test_update_high_availability_router(self):
         """Update high-availability router
 
@@ -305,7 +330,9 @@
     @decorators.idempotent_id('e1932c19-8f73-41cd-b5d2-84c7ae5d530c')
     @utils.requires_ext(extension='dvr', service='network')
     @rbac_rule_validation.action(service="neutron",
-                                 rule="update_router:distributed")
+                                 rules=["get_router", "update_router",
+                                        "update_router:distributed"],
+                                 expected_error_codes=[404, 403, 403])
     def test_update_distributed_router(self):
         """Update distributed router
 
@@ -318,7 +345,8 @@
                         distributed=False)
 
     @rbac_rule_validation.action(service="neutron",
-                                 rule="delete_router")
+                                 rules=["get_router", "delete_router"],
+                                 expected_error_codes=[404, 403])
     @decorators.idempotent_id('c0634dd5-0467-48f7-a4ae-1014d8edb2a7')
     def test_delete_router(self):
         """Delete Router
@@ -352,7 +380,9 @@
             subnet_id=subnet['id'])
 
     @rbac_rule_validation.action(service="neutron",
-                                 rule="remove_router_interface")
+                                 rules=["get_router",
+                                        "remove_router_interface"],
+                                 expected_error_codes=[404, 403])
     @decorators.idempotent_id('ff2593a4-2bff-4c27-97d3-dd3702b27dfb')
     def test_remove_router_interface(self):
         """Remove Router Interface
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 aaba1d2..1cf841d 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
@@ -18,6 +18,7 @@
 from tempest.lib.common.utils import test_utils
 from tempest.lib import decorators
 
+from patrole_tempest_plugin import rbac_exceptions
 from patrole_tempest_plugin import rbac_rule_validation
 from patrole_tempest_plugin.tests.api.network import rbac_base as base
 
@@ -80,15 +81,16 @@
                                  rule="get_security_group",
                                  expected_error_code=404)
     @decorators.idempotent_id('56335e77-aef2-4b54-86c7-7f772034b585')
-    def test_show_security_groups(self):
+    def test_show_security_group(self):
 
         with self.rbac_utils.override_role(self):
             self.security_groups_client.show_security_group(
                 self.secgroup['id'])
 
     @rbac_rule_validation.action(service="neutron",
-                                 rule="delete_security_group",
-                                 expected_error_code=404)
+                                 rules=["get_security_group",
+                                        "delete_security_group"],
+                                 expected_error_codes=[404, 403])
     @decorators.idempotent_id('0b1330fd-dd28-40f3-ad73-966052e4b3de')
     def test_delete_security_group(self):
 
@@ -99,8 +101,9 @@
             self.security_groups_client.delete_security_group(secgroup_id)
 
     @rbac_rule_validation.action(service="neutron",
-                                 rule="update_security_group",
-                                 expected_error_code=404)
+                                 rules=["get_security_group",
+                                        "update_security_group"],
+                                 expected_error_codes=[404, 403])
     @decorators.idempotent_id('56c5e4dc-f8aa-11e6-bc64-92361f002671')
     def test_update_security_group(self):
 
@@ -113,12 +116,17 @@
                 description="test description")
 
     @rbac_rule_validation.action(service="neutron",
-                                 rule="get_security_groups")
+                                 rule="get_security_group")
     @decorators.idempotent_id('fbaf8d96-ed3e-49af-b24c-5fb44f05bbb7')
     def test_list_security_groups(self):
 
         with self.rbac_utils.override_role(self):
-            self.security_groups_client.list_security_groups()
+            security_groups = self.security_groups_client.\
+                list_security_groups()
+
+        # Neutron may return an empty list if access is denied.
+        if not security_groups['security_groups']:
+            raise rbac_exceptions.RbacMalformedResponse(empty=True)
 
     @rbac_rule_validation.action(service="neutron",
                                  rule="create_security_group_rule")
@@ -129,8 +137,9 @@
             self._create_security_group_rule()
 
     @rbac_rule_validation.action(service="neutron",
-                                 rule="delete_security_group_rule",
-                                 expected_error_code=404)
+                                 rules=["get_security_group_rule",
+                                        "delete_security_group_rule"],
+                                 expected_error_codes=[404, 403])
     @decorators.idempotent_id('2262539e-b7d9-438c-acf9-a5ce0613be28')
     def test_delete_security_group_rule(self):
 
@@ -151,9 +160,14 @@
                 sec_group_rule['id'])
 
     @rbac_rule_validation.action(service="neutron",
-                                 rule="get_security_group_rules")
+                                 rule="get_security_group_rule")
     @decorators.idempotent_id('05739ab6-fa35-11e6-bc64-92361f002671')
     def test_list_security_group_rules(self):
 
         with self.rbac_utils.override_role(self):
-            self.security_group_rules_client.list_security_group_rules()
+            security_rules = self.security_group_rules_client.\
+                list_security_group_rules()
+
+        # Neutron may return an empty list if access is denied.
+        if not security_rules['security_group_rules']:
+            raise rbac_exceptions.RbacMalformedResponse(empty=True)
diff --git a/patrole_tempest_plugin/tests/api/network/test_subnetpools_rbac.py b/patrole_tempest_plugin/tests/api/network/test_subnetpools_rbac.py
index fe14c92..124b59a 100644
--- a/patrole_tempest_plugin/tests/api/network/test_subnetpools_rbac.py
+++ b/patrole_tempest_plugin/tests/api/network/test_subnetpools_rbac.py
@@ -64,7 +64,9 @@
             self._create_subnetpool()
 
     @rbac_rule_validation.action(service="neutron",
-                                 rule="create_subnetpool:shared")
+                                 rules=["create_subnetpool",
+                                        "create_subnetpool:shared"],
+                                 expected_error_codes=[403, 403])
     @decorators.idempotent_id('cf730989-0d47-40bc-b39a-99e7de484723')
     def test_create_subnetpool_shared(self):
         """Create subnetpool shared.
@@ -88,7 +90,9 @@
             self.subnetpools_client.show_subnetpool(subnetpool['id'])
 
     @rbac_rule_validation.action(service="neutron",
-                                 rule="update_subnetpool")
+                                 rules=["get_subnetpool",
+                                        "update_subnetpool"],
+                                 expected_error_codes=[404, 403])
     @decorators.idempotent_id('1e79cead-5081-4be2-a4f7-484c0f443b9b')
     def test_update_subnetpool(self):
         """Update subnetpool.
@@ -102,7 +106,9 @@
 
     @decorators.idempotent_id('a16f4e5c-0675-415f-b636-00af00638693')
     @rbac_rule_validation.action(service="neutron",
-                                 rule="update_subnetpool:is_default")
+                                 rules=["update_subnetpool",
+                                        "update_subnetpool:is_default"],
+                                 expected_error_codes=[403, 403])
     def test_update_subnetpool_is_default(self):
         """Update default subnetpool.
 
@@ -122,7 +128,9 @@
                 default_pool['id'], description=original_desc, is_default=True)
 
     @rbac_rule_validation.action(service="neutron",
-                                 rule="delete_subnetpool")
+                                 rules=["get_subnetpool",
+                                        "delete_subnetpool"],
+                                 expected_error_codes=[404, 403])
     @decorators.idempotent_id('50f5944e-43e5-457b-ab50-fb48a73f0d3e')
     def test_delete_subnetpool(self):
         """Delete subnetpool.
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 bc36c21..77d4b42 100644
--- a/patrole_tempest_plugin/tests/api/network/test_subnets_rbac.py
+++ b/patrole_tempest_plugin/tests/api/network/test_subnets_rbac.py
@@ -17,6 +17,7 @@
 from tempest.lib.common.utils import data_utils
 from tempest.lib import decorators
 
+from patrole_tempest_plugin import rbac_exceptions
 from patrole_tempest_plugin import rbac_rule_validation
 from patrole_tempest_plugin.tests.api.network import rbac_base as base
 
@@ -49,7 +50,8 @@
 
     @decorators.idempotent_id('c02618e7-bb20-4abd-83c8-6eec2af08752')
     @rbac_rule_validation.action(service="neutron",
-                                 rule="get_subnet")
+                                 rule="get_subnet",
+                                 expected_error_code=404)
     def test_show_subnet(self):
         """Show subnet.
 
@@ -67,11 +69,16 @@
         RBAC test for the neutron "get_subnet" policy
         """
         with self.rbac_utils.override_role(self):
-            self.subnets_client.list_subnets()
+            subnets = self.subnets_client.list_subnets()
+
+        # Neutron may return an empty list if access is denied.
+        if not subnets['subnets']:
+            raise rbac_exceptions.RbacMalformedResponse(empty=True)
 
     @decorators.idempotent_id('f36cd821-dd22-4bd0-b43d-110fc4b553eb')
     @rbac_rule_validation.action(service="neutron",
-                                 rule="update_subnet")
+                                 rules=["get_subnet", "update_subnet"],
+                                 expected_error_codes=[404, 403])
     def test_update_subnet(self):
         """Update subnet.
 
@@ -85,7 +92,8 @@
 
     @decorators.idempotent_id('bcfc7153-bbd1-43a4-a908-b3e1b0cde0dc')
     @rbac_rule_validation.action(service="neutron",
-                                 rule="delete_subnet")
+                                 rules=["get_subnet", "delete_subnet"],
+                                 expected_error_codes=[404, 403])
     def test_delete_subnet(self):
         """Delete subnet.
 
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 bdba242..2e275dc 100644
--- a/patrole_tempest_plugin/tests/unit/test_rbac_rule_validation.py
+++ b/patrole_tempest_plugin/tests/unit/test_rbac_rule_validation.py
@@ -95,11 +95,12 @@
     @mock.patch.object(rbac_rv, 'policy_authority', autospec=True)
     def test_rule_validation_forbidden_negative(self, mock_authority,
                                                 mock_log):
-        """Test Forbidden error is thrown and have permission fails.
+        """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 Forbidden exception should be
-        raised.
+        allowed to perform the action, then the RbacUnderPermissionException
+        exception should be raised.
         """
         mock_authority.PolicyAuthority.return_value.allowed.return_value = True
 
@@ -109,8 +110,9 @@
 
         test_re = ("Role Member was not allowed to perform the following "
                    "actions: \[%s\].*" % (mock.sentinel.action))
-        self.assertRaisesRegex(exceptions.Forbidden, test_re, test_policy,
-                               self.mock_test_args)
+        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)
@@ -149,8 +151,9 @@
 
         test_re = ("Role Member was not allowed to perform the following "
                    "actions: \[%s\].*" % (mock.sentinel.action))
-        self.assertRaisesRegex(exceptions.Forbidden, test_re, test_policy,
-                               self.mock_test_args)
+        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)
@@ -190,8 +193,9 @@
 
         test_re = ("Role Member was not allowed to perform the following "
                    "actions: \[%s\].*" % (mock.sentinel.action))
-        self.assertRaisesRegex(exceptions.Forbidden, test_re, test_policy,
-                               self.mock_test_args)
+        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)
@@ -211,17 +215,15 @@
         def test_policy(*args):
             raise exceptions.Forbidden('Test message')
 
-        error_msg = ("An unexpected exception has occurred during test: "
-                     "test_policy. Exception was: Forbidden\nDetails: Test "
-                     "message")
+        error_re = r'Expected .* to be raised but .* was raised instead'
 
         for allowed in [True, False]:
             mock_authority.PolicyAuthority.return_value.allowed.\
                 return_value = allowed
-            self.assertRaisesRegex(exceptions.Forbidden, 'Test message',
-                                   test_policy, self.mock_test_args)
-            self.assertIn(error_msg, mock_log.error.mock_calls[0][1][0])
-            mock_log.error.reset_mock()
+            self.assertRaisesRegex(
+                rbac_exceptions.RbacExpectedWrongException, error_re,
+                test_policy, self.mock_test_args)
+            self.assertTrue(mock_log.error.called)
 
     @mock.patch.object(rbac_rv, 'LOG', autospec=True)
     @mock.patch.object(rbac_rv, 'policy_authority', autospec=True)
@@ -256,8 +258,9 @@
             error_re = expected_errors[pos]
 
             if error_re:
-                self.assertRaisesRegex(exceptions.Forbidden, error_re,
-                                       test_policy, self.mock_test_args)
+                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)
@@ -278,7 +281,7 @@
     @mock.patch.object(rbac_rv, 'policy_authority', autospec=True)
     def test_rule_validation_overpermission_negative(self, mock_authority,
                                                      mock_log):
-        """Test that OverPermission is correctly handled.
+        """Test that RbacOverPermissionException is correctly handled.
 
         Tests that case where no exception is thrown but the Patrole framework
         says that the role should not be allowed to perform the policy action.
@@ -299,7 +302,7 @@
             test_policy_expect_forbidden, test_policy_expect_not_found):
 
             error_re = ".*OverPermission: .* \[%s\]$" % mock.sentinel.action
-            self.assertRaisesRegex(rbac_exceptions.RbacOverPermission,
+            self.assertRaisesRegex(rbac_exceptions.RbacOverPermissionException,
                                    error_re, test_policy, self.mock_test_args)
             self.assertRegex(mock_log.error.mock_calls[0][1][0], error_re)
             mock_log.error.reset_mock()
@@ -341,7 +344,7 @@
 
     @mock.patch.object(rbac_rv, 'policy_authority', autospec=True)
     def test_get_exception_type_403(self, _):
-        """Test that getting a 404 exception type returns Forbidden."""
+        """Test that getting a 403 exception type returns Forbidden."""
         expected_exception = exceptions.Forbidden
         expected_irregular_msg = None
 
@@ -462,7 +465,8 @@
     def test_rule_validation_multi_policy_overpermission_failure(
             self, mock_authority, mock_log):
         """Test that when expected result is unauthorized and test passes that
-        the overall evaluation results in an OverPermission getting raised.
+        the overall evaluation results in an RbacOverPermissionException
+        getting raised.
         """
 
         rules = [
@@ -480,8 +484,9 @@
                 allowed_list)
 
             error_re = ".*OverPermission: .* \[%s\]$" % fail_on_action
-            self.assertRaisesRegex(rbac_exceptions.RbacOverPermission,
-                                   error_re, test_policy, self.mock_test_args)
+            self.assertRaisesRegex(
+                rbac_exceptions.RbacOverPermissionException, error_re,
+                test_policy, self.mock_test_args)
             mock_log.debug.assert_any_call(
                 "%s: Expecting %d to be raised for policy name: %s",
                 'test_policy', 403, fail_on_action)
@@ -531,7 +536,7 @@
             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 a Forbidden getting raised.
+        results in a RbacUnderPermissionException getting raised.
         """
 
         # NOTE: Avoid mock.sentinel here due to weird sorting with them.
@@ -549,8 +554,9 @@
                     "actions: %s. Expected allowed actions: %s. Expected "
                     "disallowed actions: []." % (rules, rules)).replace(
                         '[', '\[').replace(']', '\]')
-        self.assertRaisesRegex(exceptions.Forbidden, error_re, test_policy,
-                               self.mock_test_args)
+        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)
 
@@ -558,7 +564,7 @@
     @mock.patch.object(rbac_rv, 'policy_authority', autospec=True)
     def test_rule_validation_multi_actions_forbidden(
             self, mock_authority, mock_log):
-        """Test that when the expected result is forbidden because
+        """Test that when the expected result is Forbidden because
         two of the actions fail and the first action specifies 403,
         verify that the overall evaluation results in success.
         """
diff --git a/playbooks/patrole-multinode-admin/post.yaml b/playbooks/patrole-multinode-admin/post.yaml
deleted file mode 100644
index dac8753..0000000
--- a/playbooks/patrole-multinode-admin/post.yaml
+++ /dev/null
@@ -1,80 +0,0 @@
-- hosts: primary
-  tasks:
-
-    - name: Copy files from {{ ansible_user_dir }}/workspace/ on node
-      synchronize:
-        src: '{{ ansible_user_dir }}/workspace/'
-        dest: '{{ zuul.executor.log_root }}'
-        mode: pull
-        copy_links: true
-        verify_host: true
-        rsync_opts:
-          - --include=**/*nose_results.html
-          - --include=*/
-          - --exclude=*
-          - --prune-empty-dirs
-
-    - name: Copy files from {{ ansible_user_dir }}/workspace/ on node
-      synchronize:
-        src: '{{ ansible_user_dir }}/workspace/'
-        dest: '{{ zuul.executor.log_root }}'
-        mode: pull
-        copy_links: true
-        verify_host: true
-        rsync_opts:
-          - --include=**/*testr_results.html.gz
-          - --include=*/
-          - --exclude=*
-          - --prune-empty-dirs
-
-    - name: Copy files from {{ ansible_user_dir }}/workspace/ on node
-      synchronize:
-        src: '{{ ansible_user_dir }}/workspace/'
-        dest: '{{ zuul.executor.log_root }}'
-        mode: pull
-        copy_links: true
-        verify_host: true
-        rsync_opts:
-          - --include=/.testrepository/tmp*
-          - --include=*/
-          - --exclude=*
-          - --prune-empty-dirs
-
-    - name: Copy files from {{ ansible_user_dir }}/workspace/ on node
-      synchronize:
-        src: '{{ ansible_user_dir }}/workspace/'
-        dest: '{{ zuul.executor.log_root }}'
-        mode: pull
-        copy_links: true
-        verify_host: true
-        rsync_opts:
-          - --include=**/*testrepository.subunit.gz
-          - --include=*/
-          - --exclude=*
-          - --prune-empty-dirs
-
-    - name: Copy files from {{ ansible_user_dir }}/workspace/ on node
-      synchronize:
-        src: '{{ ansible_user_dir }}/workspace/'
-        dest: '{{ zuul.executor.log_root }}/tox'
-        mode: pull
-        copy_links: true
-        verify_host: true
-        rsync_opts:
-          - --include=/.tox/*/log/*
-          - --include=*/
-          - --exclude=*
-          - --prune-empty-dirs
-
-    - name: Copy files from {{ ansible_user_dir }}/workspace/ on node
-      synchronize:
-        src: '{{ ansible_user_dir }}/workspace/'
-        dest: '{{ zuul.executor.log_root }}'
-        mode: pull
-        copy_links: true
-        verify_host: true
-        rsync_opts:
-          - --include=/logs/**
-          - --include=*/
-          - --exclude=*
-          - --prune-empty-dirs
diff --git a/playbooks/patrole-multinode-admin/run.yaml b/playbooks/patrole-multinode-admin/run.yaml
deleted file mode 100644
index bece4e2..0000000
--- a/playbooks/patrole-multinode-admin/run.yaml
+++ /dev/null
@@ -1,63 +0,0 @@
-- hosts: primary
-  name: Autoconverted job legacy-tempest-dsvm-patrole-multinode-admin from old job
-    gate-tempest-dsvm-patrole-multinode-admin-ubuntu-xenial-nv
-  tasks:
-
-    - name: Ensure legacy workspace directory
-      file:
-        path: '{{ ansible_user_dir }}/workspace'
-        state: directory
-
-    - shell:
-        cmd: |
-          set -e
-          set -x
-          cat > clonemap.yaml << EOF
-          clonemap:
-            - name: openstack-infra/devstack-gate
-              dest: devstack-gate
-          EOF
-          /usr/zuul-env/bin/zuul-cloner -m clonemap.yaml --cache-dir /opt/git \
-              git://git.openstack.org \
-              openstack-infra/devstack-gate
-        executable: /bin/bash
-        chdir: '{{ ansible_user_dir }}/workspace'
-      environment: '{{ zuul | zuul_legacy_vars }}'
-
-    - shell:
-        cmd: |
-          set -e
-          set -x
-          cat << 'EOF' >>"/tmp/dg-local.conf"
-          [[local|localrc]]
-          enable_plugin patrole git://git.openstack.org/openstack/patrole
-          TEMPEST_PLUGINS='/opt/stack/new/patrole'
-          # Needed by Patrole devstack plugin
-          RBAC_TEST_ROLE=admin
-          EOF
-        executable: /bin/bash
-        chdir: '{{ ansible_user_dir }}/workspace'
-      environment: '{{ zuul | zuul_legacy_vars }}'
-
-    - shell:
-        cmd: |
-          set -e
-          set -x
-          export PYTHONUNBUFFERED=true
-          # Ensure that tempest set up is executed, but do not automatically
-          # execute tempest tests; they are executed in post_test_hook.
-          export DEVSTACK_GATE_TEMPEST=1
-          export DEVSTACK_GATE_NEUTRON=1
-          export DEVSTACK_GATE_TOPOLOGY="multinode"
-          export DEVSTACK_GATE_TEMPEST_REGEX='(?=.*\[.*\bslow\b.*\])(^patrole_tempest_plugin\.tests\.api)'
-          export DEVSTACK_GATE_TEMPEST_ALL_PLUGINS=1
-          export PROJECTS="openstack/patrole $PROJECTS"
-          export BRANCH_OVERRIDE=default
-          if [ "$BRANCH_OVERRIDE" != "default" ] ; then
-              export OVERRIDE_ZUUL_BRANCH=$BRANCH_OVERRIDE
-          fi
-          cp devstack-gate/devstack-vm-gate-wrap.sh ./safe-devstack-vm-gate-wrap.sh
-          ./safe-devstack-vm-gate-wrap.sh
-        executable: /bin/bash
-        chdir: '{{ ansible_user_dir }}/workspace'
-      environment: '{{ zuul | zuul_legacy_vars }}'
diff --git a/playbooks/patrole-multinode-member/post.yaml b/playbooks/patrole-multinode-member/post.yaml
deleted file mode 100644
index dac8753..0000000
--- a/playbooks/patrole-multinode-member/post.yaml
+++ /dev/null
@@ -1,80 +0,0 @@
-- hosts: primary
-  tasks:
-
-    - name: Copy files from {{ ansible_user_dir }}/workspace/ on node
-      synchronize:
-        src: '{{ ansible_user_dir }}/workspace/'
-        dest: '{{ zuul.executor.log_root }}'
-        mode: pull
-        copy_links: true
-        verify_host: true
-        rsync_opts:
-          - --include=**/*nose_results.html
-          - --include=*/
-          - --exclude=*
-          - --prune-empty-dirs
-
-    - name: Copy files from {{ ansible_user_dir }}/workspace/ on node
-      synchronize:
-        src: '{{ ansible_user_dir }}/workspace/'
-        dest: '{{ zuul.executor.log_root }}'
-        mode: pull
-        copy_links: true
-        verify_host: true
-        rsync_opts:
-          - --include=**/*testr_results.html.gz
-          - --include=*/
-          - --exclude=*
-          - --prune-empty-dirs
-
-    - name: Copy files from {{ ansible_user_dir }}/workspace/ on node
-      synchronize:
-        src: '{{ ansible_user_dir }}/workspace/'
-        dest: '{{ zuul.executor.log_root }}'
-        mode: pull
-        copy_links: true
-        verify_host: true
-        rsync_opts:
-          - --include=/.testrepository/tmp*
-          - --include=*/
-          - --exclude=*
-          - --prune-empty-dirs
-
-    - name: Copy files from {{ ansible_user_dir }}/workspace/ on node
-      synchronize:
-        src: '{{ ansible_user_dir }}/workspace/'
-        dest: '{{ zuul.executor.log_root }}'
-        mode: pull
-        copy_links: true
-        verify_host: true
-        rsync_opts:
-          - --include=**/*testrepository.subunit.gz
-          - --include=*/
-          - --exclude=*
-          - --prune-empty-dirs
-
-    - name: Copy files from {{ ansible_user_dir }}/workspace/ on node
-      synchronize:
-        src: '{{ ansible_user_dir }}/workspace/'
-        dest: '{{ zuul.executor.log_root }}/tox'
-        mode: pull
-        copy_links: true
-        verify_host: true
-        rsync_opts:
-          - --include=/.tox/*/log/*
-          - --include=*/
-          - --exclude=*
-          - --prune-empty-dirs
-
-    - name: Copy files from {{ ansible_user_dir }}/workspace/ on node
-      synchronize:
-        src: '{{ ansible_user_dir }}/workspace/'
-        dest: '{{ zuul.executor.log_root }}'
-        mode: pull
-        copy_links: true
-        verify_host: true
-        rsync_opts:
-          - --include=/logs/**
-          - --include=*/
-          - --exclude=*
-          - --prune-empty-dirs
diff --git a/playbooks/patrole-multinode-member/run.yaml b/playbooks/patrole-multinode-member/run.yaml
deleted file mode 100644
index 4c7b70f..0000000
--- a/playbooks/patrole-multinode-member/run.yaml
+++ /dev/null
@@ -1,63 +0,0 @@
-- hosts: primary
-  name: Autoconverted job legacy-tempest-dsvm-patrole-multinode-member from old job
-    gate-tempest-dsvm-patrole-multinode-member-ubuntu-xenial-nv
-  tasks:
-
-    - name: Ensure legacy workspace directory
-      file:
-        path: '{{ ansible_user_dir }}/workspace'
-        state: directory
-
-    - shell:
-        cmd: |
-          set -e
-          set -x
-          cat > clonemap.yaml << EOF
-          clonemap:
-            - name: openstack-infra/devstack-gate
-              dest: devstack-gate
-          EOF
-          /usr/zuul-env/bin/zuul-cloner -m clonemap.yaml --cache-dir /opt/git \
-              git://git.openstack.org \
-              openstack-infra/devstack-gate
-        executable: /bin/bash
-        chdir: '{{ ansible_user_dir }}/workspace'
-      environment: '{{ zuul | zuul_legacy_vars }}'
-
-    - shell:
-        cmd: |
-          set -e
-          set -x
-          cat << 'EOF' >>"/tmp/dg-local.conf"
-          [[local|localrc]]
-          enable_plugin patrole git://git.openstack.org/openstack/patrole
-          TEMPEST_PLUGINS='/opt/stack/new/patrole'
-          # Needed by Patrole devstack plugin
-          RBAC_TEST_ROLE=member
-          EOF
-        executable: /bin/bash
-        chdir: '{{ ansible_user_dir }}/workspace'
-      environment: '{{ zuul | zuul_legacy_vars }}'
-
-    - shell:
-        cmd: |
-          set -e
-          set -x
-          export PYTHONUNBUFFERED=true
-          # Ensure that tempest set up is executed, but do not automatically
-          # execute tempest tests; they are executed in post_test_hook.
-          export DEVSTACK_GATE_TEMPEST=1
-          export DEVSTACK_GATE_NEUTRON=1
-          export DEVSTACK_GATE_TOPOLOGY="multinode"
-          export DEVSTACK_GATE_TEMPEST_REGEX='(?=.*\[.*\bslow\b.*\])(^patrole_tempest_plugin\.tests\.api)'
-          export DEVSTACK_GATE_TEMPEST_ALL_PLUGINS=1
-          export PROJECTS="openstack/patrole $PROJECTS"
-          export BRANCH_OVERRIDE=default
-          if [ "$BRANCH_OVERRIDE" != "default" ] ; then
-              export OVERRIDE_ZUUL_BRANCH=$BRANCH_OVERRIDE
-          fi
-          cp devstack-gate/devstack-vm-gate-wrap.sh ./safe-devstack-vm-gate-wrap.sh
-          ./safe-devstack-vm-gate-wrap.sh
-        executable: /bin/bash
-        chdir: '{{ ansible_user_dir }}/workspace'
-      environment: '{{ zuul | zuul_legacy_vars }}'