Merge "Add fixed ip cleanup to test_add_fixed_ip"
diff --git a/.zuul.yaml b/.zuul.yaml
index 2619ed7..21b5679 100644
--- a/.zuul.yaml
+++ b/.zuul.yaml
@@ -1,7 +1,7 @@
 - job:
     name: patrole-base
     parent: devstack-tempest
-    description: Patrole base job for admin and Member roles.
+    description: Patrole base job for admin and member roles.
     required-projects:
       - name: openstack/tempest
       - name: openstack/patrole
@@ -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
@@ -54,7 +73,7 @@
 - job:
     name: patrole-member
     parent: patrole-base
-    description: Patrole job for Member role.
+    description: Patrole job for member role.
     # This currently works from stable/pike onward.
     branches:
       - master
@@ -62,7 +81,7 @@
       - stable/pike
     vars:
       devstack_localrc:
-        RBAC_TEST_ROLE: Member
+        RBAC_TEST_ROLE: member
 
 - job:
     name: patrole-member-queens
@@ -77,28 +96,28 @@
 - 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
     parent: patrole-base
-    description: Patrole py3 job for Member role.
+    description: Patrole py35 job for member role.
     vars:
       devstack_localrc:
-        # Use Member for py3 because arguably negative testing is more
+        # Use member for py35 because arguably negative testing is more
         # important than admin, which is already covered by patrole-admin job.
-        RBAC_TEST_ROLE: Member
+        RBAC_TEST_ROLE: member
         USE_PYTHON3: true
       devstack_services:
         s-account: false
@@ -123,7 +142,3 @@
       jobs:
         - patrole-admin
         - patrole-member
-        - patrole-member-queens
-        - patrole-member-pike
-        - patrole-py35-member
-        - openstack-tox-lower-constraints
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/devstack/plugin.sh b/devstack/plugin.sh
index d56c963..bd0068b 100644
--- a/devstack/plugin.sh
+++ b/devstack/plugin.sh
@@ -13,16 +13,13 @@
 function install_patrole_tempest_plugin {
     setup_package $PATROLE_DIR -e
 
-    if [[ "$RBAC_TEST_ROLE" == "member" ]]; then
-        RBAC_TEST_ROLE="Member"
-    fi
-
-    iniset $TEMPEST_CONFIG patrole enable_rbac True
-    iniset $TEMPEST_CONFIG patrole rbac_test_role $RBAC_TEST_ROLE
-
     if [[ ${DEVSTACK_SERIES} == 'pike' ]]; then
+        if [[ "$RBAC_TEST_ROLE" == "member" ]]; then
+            RBAC_TEST_ROLE="Member"
+        fi
+
         # Policies used by Patrole testing that were changed in a backwards-incompatible way.
-        # TODO(fmontei): Remove these once stable/pike becomes EOL.
+        # TODO(felipemonteiro): Remove these once stable/pike becomes EOL.
         iniset $TEMPEST_CONFIG policy-feature-enabled create_port_fixed_ips_ip_address_policy False
         iniset $TEMPEST_CONFIG policy-feature-enabled update_port_fixed_ips_ip_address_policy False
         iniset $TEMPEST_CONFIG policy-feature-enabled limits_extension_used_limits_policy False
@@ -30,6 +27,15 @@
         iniset $TEMPEST_CONFIG policy-feature-enabled volume_extension_volume_actions_reserve_policy False
         iniset $TEMPEST_CONFIG policy-feature-enabled volume_extension_volume_actions_unreserve_policy False
     fi
+
+    if [[ ${DEVSTACK_SERIES} == 'queens' ]]; then
+        if [[ "$RBAC_TEST_ROLE" == "member" ]]; then
+            RBAC_TEST_ROLE="Member"
+        fi
+    fi
+
+    iniset $TEMPEST_CONFIG patrole enable_rbac True
+    iniset $TEMPEST_CONFIG patrole rbac_test_role $RBAC_TEST_ROLE
 }
 
 if is_service_enabled tempest; then
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/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/patrole_tempest_plugin/rbac_rule_validation.py b/patrole_tempest_plugin/rbac_rule_validation.py
index 69a43ea..23a210a 100644
--- a/patrole_tempest_plugin/rbac_rule_validation.py
+++ b/patrole_tempest_plugin/rbac_rule_validation.py
@@ -33,11 +33,13 @@
 LOG = logging.getLogger(__name__)
 
 _SUPPORTED_ERROR_CODES = [403, 404]
+_DEFAULT_ERROR_CODE = 403
 
 RBACLOG = logging.getLogger('rbac_reporting')
 
 
-def action(service, rule='', rules=None, expected_error_code=403,
+def action(service, rule='', rules=None,
+           expected_error_code=_DEFAULT_ERROR_CODE, expected_error_codes=None,
            extra_target_data=None):
     """A decorator for verifying OpenStack policy enforcement.
 
@@ -80,16 +82,36 @@
 
             Patrole currently only supports custom JSON policy files.
 
-    :param int expected_error_code: Overrides default value of 403 (Forbidden)
-        with endpoint-specific error code. Currently only supports 403 and 404.
-        Support for 404 is needed because some services, like Neutron,
-        intentionally throw a 404 for security reasons.
+    :param int expected_error_code: (DEPRECATED) Overrides default value of 403
+        (Forbidden) with endpoint-specific error code. Currently only supports
+        403 and 404. Support for 404 is needed because some services, like
+        Neutron, intentionally throw a 404 for security reasons.
 
         .. warning::
 
             A 404 should not be provided *unless* the endpoint masks a
             ``Forbidden`` exception as a ``NotFound`` exception.
 
+    :param list expected_error_codes: When the ``rules`` list parameter is
+        used, then this list indicates the expected error code to use if one
+        of the rules does not allow the role being tested. This list must
+        coincide with and its elements remain in the same order as the rules
+        in the rules list.
+
+        Example::
+
+            rules=["api_action1", "api_action2"]
+            expected_error_codes=[404, 403]
+
+        a) If api_action1 fails and api_action2 passes, then the expected
+           error code is 404.
+        b) if api_action2 fails and api_action1 passes, then the expected
+           error code is 403.
+        c) if both api_action1 and api_action2 fail, then the expected error
+           code is the first error seen (404).
+
+        If an error code is missing from the list, it is defaulted to 403.
+
     :param dict extra_target_data: Dictionary, keyed with ``oslo.policy``
         generic check names, whose values are string literals that reference
         nested ``tempest.test.BaseTestCase`` attributes. Used by
@@ -118,7 +140,9 @@
     if extra_target_data is None:
         extra_target_data = {}
 
-    rules = _prepare_rules(rule, rules)
+    rules, expected_error_codes = _prepare_multi_policy(rule, rules,
+                                                        expected_error_code,
+                                                        expected_error_codes)
 
     def decorator(test_func):
         role = CONF.patrole.rbac_test_role
@@ -141,8 +165,18 @@
                     disallowed_rules.append(rule)
                 allowed = allowed and _allowed
 
+            exp_error_code = expected_error_code
+            if disallowed_rules:
+                # Choose the first disallowed rule and expect the error
+                # code corresponding to it.
+                first_error_index = rules.index(disallowed_rules[0])
+                exp_error_code = expected_error_codes[first_error_index]
+                LOG.debug("%s: Expecting %d to be raised for policy name: %s",
+                          test_func.__name__, exp_error_code,
+                          disallowed_rules[0])
+
             expected_exception, irregular_msg = _get_exception_type(
-                expected_error_code)
+                exp_error_code)
 
             test_status = 'Allowed'
 
@@ -202,7 +236,32 @@
     return decorator
 
 
-def _prepare_rules(rule, rules):
+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 "
+                   "`expected_error_codes` list.")
+            raise ValueError(msg)
+        if len(rules) != len(exp_error_codes):
+            msg = ("The `expected_error_codes` list is not the same length "
+                   "as the `rules` list.")
+            raise ValueError(msg)
+        if exp_error_code:
+            deprecation_msg = (
+                "The `exp_error_code` argument has been deprecated in favor "
+                "of `exp_error_codes` and will be removed in a future "
+                "version.")
+            versionutils.report_deprecated_feature(LOG, deprecation_msg)
+            LOG.debug("The `exp_error_codes` argument will be used instead of "
+                      "`exp_error_code`.")
+        if not isinstance(exp_error_codes, (tuple, list)):
+            exp_error_codes = [exp_error_codes]
+    else:
+        exp_error_codes = []
+        if exp_error_code:
+            exp_error_codes.append(exp_error_code)
+
     if rules is None:
         rules = []
     elif not isinstance(rules, (tuple, list)):
@@ -216,7 +275,18 @@
             LOG.debug("The `rules` argument will be used instead of `rule`.")
         else:
             rules.append(rule)
-    return rules
+
+    # Fill in the exp_error_codes if needed. This is needed for the scenarios
+    # where no exp_error_codes array is provided, so the error codes must be
+    # set to the default error code value and there must be the same number
+    # of error codes as rules.
+    num_ecs = len(exp_error_codes)
+    num_rules = len(rules)
+    if (num_ecs < num_rules):
+        for i in range(num_rules - num_ecs):
+            exp_error_codes.append(_DEFAULT_ERROR_CODE)
+
+    return rules, exp_error_codes
 
 
 def _is_authorized(test_obj, service, rule, extra_target_data):
diff --git a/patrole_tempest_plugin/rbac_utils.py b/patrole_tempest_plugin/rbac_utils.py
index 2ef88ca..6c40aa1 100644
--- a/patrole_tempest_plugin/rbac_utils.py
+++ b/patrole_tempest_plugin/rbac_utils.py
@@ -147,18 +147,25 @@
             test_obj.os_primary.auth_provider.set_auth()
 
     def _get_roles_by_name(self):
-        available_roles = self.admin_roles_client.list_roles()
-        admin_role_id = rbac_role_id = None
+        available_roles = self.admin_roles_client.list_roles()['roles']
+        role_map = {r['name']: r['id'] for r in available_roles}
+        LOG.debug('Available roles: %s', list(role_map.keys()))
 
-        for role in available_roles['roles']:
-            if role['name'] == CONF.patrole.rbac_test_role:
-                rbac_role_id = role['id']
-            if role['name'] == CONF.identity.admin_role:
-                admin_role_id = role['id']
+        admin_role_id = role_map.get(CONF.identity.admin_role)
+        rbac_role_id = role_map.get(CONF.patrole.rbac_test_role)
 
         if not all([admin_role_id, rbac_role_id]):
-            msg = ("Roles defined by `[patrole] rbac_test_role` and "
-                   "`[identity] admin_role` must be defined in the system.")
+            missing_roles = []
+            msg = ("Could not find `[patrole] rbac_test_role` or "
+                   "`[identity] admin_role`, both of which are required for "
+                   "RBAC testing.")
+            if not admin_role_id:
+                missing_roles.append(CONF.identity.admin_role)
+            if not rbac_role_id:
+                missing_roles.append(CONF.patrole.rbac_test_role)
+            msg += " Following roles were not found: %s." % (
+                ", ".join(missing_roles))
+            msg += " Available roles: %s." % ", ".join(list(role_map.keys()))
             raise rbac_exceptions.RbacResourceSetupFailed(msg)
 
         self.admin_role_id = admin_role_id
@@ -226,4 +233,6 @@
 
     :returns: True if ``rbac_test_role`` is the admin role.
     """
+    # TODO(felipemonteiro): Make this more robust via a context is admin
+    # lookup.
     return CONF.patrole.rbac_test_role == CONF.identity.admin_role
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/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 ab85745..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
@@ -330,7 +358,8 @@
             self.routers_client.delete_router(router['id'])
 
     @rbac_rule_validation.action(service="neutron",
-                                 rule="add_router_interface")
+                                 rules=["get_router", "add_router_interface"],
+                                 expected_error_codes=[404, 403])
     @decorators.idempotent_id('a0627778-d68d-4913-881b-e345360cca19')
     def test_add_router_interface(self):
         """Add Router Interface
@@ -351,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/api/volume/test_volume_actions_rbac.py b/patrole_tempest_plugin/tests/api/volume/test_volume_actions_rbac.py
index dcc2bd5..a4fc3fd 100644
--- a/patrole_tempest_plugin/tests/api/volume/test_volume_actions_rbac.py
+++ b/patrole_tempest_plugin/tests/api/volume/test_volume_actions_rbac.py
@@ -106,31 +106,6 @@
         waiters.wait_for_volume_resource_status(
             self.volumes_client, volume_id, 'available')
 
-    @decorators.attr(type=["slow"])
-    @utils.services('image')
-    @rbac_rule_validation.action(
-        service="cinder",
-        rule="volume_extension:volume_actions:upload_image")
-    @decorators.idempotent_id('b0d0da46-903c-4445-893e-20e680d68b50')
-    def test_volume_upload(self):
-        # TODO(felipemonteiro): The ``upload_volume`` endpoint also enforces
-        # "volume:copy_volume_to_image" but is not currently contained in
-        # Cinder's policy.json.
-        image_name = data_utils.rand_name(self.__class__.__name__ + '-Image')
-
-        with self.rbac_utils.override_role(self):
-            body = self.volumes_client.upload_volume(
-                self.volume['id'], image_name=image_name, visibility="private",
-                disk_format=CONF.volume.disk_format)['os-volume_upload_image']
-        image_id = body["image_id"]
-        self.addCleanup(test_utils.call_and_ignore_notfound_exc,
-                        self.image_client.delete_image,
-                        image_id)
-        waiters.wait_for_image_status(self.image_client, image_id,
-                                      'active')
-        waiters.wait_for_volume_resource_status(self.volumes_client,
-                                                self.volume['id'], 'available')
-
     @rbac_rule_validation.action(service="cinder",
                                  rule="volume:update_readonly_flag")
     @decorators.idempotent_id('2750717a-f250-4e41-9e09-02624aad6ff8')
@@ -141,16 +116,6 @@
         self.addCleanup(self.volumes_client.update_volume_readonly,
                         self.volume['id'], readonly=False)
 
-    @decorators.idempotent_id('72bab13c-dfaf-4b6d-a132-c83a85fb1776')
-    @rbac_rule_validation.action(
-        service="cinder",
-        rule="volume_extension:volume_unmanage")
-    def test_unmanage_volume(self):
-        volume = self.create_volume()
-
-        with self.rbac_utils.override_role(self):
-            self.volumes_client.unmanage_volume(volume['id'])
-
     @decorators.idempotent_id('59b783c0-f4ef-430c-8a90-1bad97d4ec5c')
     @rbac_rule_validation.action(service="cinder",
                                  rule="volume:update")
@@ -253,6 +218,35 @@
         super(VolumesActionsV310RbacTest, cls).setup_clients()
         cls.image_client = cls.os_primary.image_client_v2
 
+    @classmethod
+    def resource_setup(cls):
+        super(VolumesActionsV310RbacTest, cls).resource_setup()
+        cls.volume = cls.create_volume()
+
+    @decorators.attr(type=["slow"])
+    @utils.services('image')
+    @rbac_rule_validation.action(
+        service="cinder",
+        rule="volume_extension:volume_actions:upload_image")
+    @decorators.idempotent_id('b0d0da46-903c-4445-893e-20e680d68b50')
+    def test_volume_upload_image(self):
+        # TODO(felipemonteiro): The ``upload_volume`` endpoint also enforces
+        # "volume:copy_volume_to_image".
+        image_name = data_utils.rand_name(self.__class__.__name__ + '-Image')
+
+        with self.rbac_utils.override_role(self):
+            body = self.volumes_client.upload_volume(
+                self.volume['id'], image_name=image_name, visibility="private",
+                disk_format=CONF.volume.disk_format)['os-volume_upload_image']
+        image_id = body["image_id"]
+        self.addCleanup(test_utils.call_and_ignore_notfound_exc,
+                        self.image_client.delete_image,
+                        image_id)
+        waiters.wait_for_image_status(self.image_client, image_id,
+                                      'active')
+        waiters.wait_for_volume_resource_status(self.volumes_client,
+                                                self.volume['id'], 'available')
+
     @decorators.attr(type=["slow"])
     @utils.services('image')
     @rbac_rule_validation.action(
@@ -261,12 +255,11 @@
     @decorators.idempotent_id('578a84dd-a6bd-4f97-a418-4a0c3c272c08')
     def test_volume_upload_public(self):
         # This also enforces "volume_extension:volume_actions:upload_image".
-        volume = self.create_volume()
         image_name = data_utils.rand_name(self.__class__.__name__ + '-Image')
 
         with self.rbac_utils.override_role(self):
             body = self.volumes_client.upload_volume(
-                volume['id'], image_name=image_name, visibility="public",
+                self.volume['id'], image_name=image_name, visibility="public",
                 disk_format=CONF.volume.disk_format)['os-volume_upload_image']
             image_id = body["image_id"]
         self.addCleanup(test_utils.call_and_ignore_notfound_exc,
@@ -275,7 +268,7 @@
         waiters.wait_for_image_status(self.image_client, image_id,
                                       'active')
         waiters.wait_for_volume_resource_status(self.volumes_client,
-                                                volume['id'], 'available')
+                                                self.volume['id'], 'available')
 
 
 class VolumesActionsV312RbacTest(rbac_base.BaseVolumeRbacTest):
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 0a4c44b..2ae860c 100644
--- a/patrole_tempest_plugin/tests/unit/test_rbac_rule_validation.py
+++ b/patrole_tempest_plugin/tests/unit/test_rbac_rule_validation.py
@@ -436,7 +436,8 @@
 
         rules = [mock.sentinel.action1, mock.sentinel.action2]
 
-        @rbac_rv.action(mock.sentinel.service, rules=rules)
+        @rbac_rv.action(mock.sentinel.service, rules=rules,
+                        expected_error_codes=[403, 403])
         def test_policy(*args):
             pass
 
@@ -454,8 +455,10 @@
         rules = [
             mock.sentinel.action1, mock.sentinel.action2, mock.sentinel.action3
         ]
+        exp_ecodes = [403, 403, 403]
 
-        @rbac_rv.action(mock.sentinel.service, rules=rules)
+        @rbac_rv.action(mock.sentinel.service, rules=rules,
+                        expected_error_codes=exp_ecodes)
         def test_policy(*args):
             pass
 
@@ -466,6 +469,9 @@
             error_re = ".*OverPermission: .* \[%s\]$" % fail_on_action
             self.assertRaisesRegex(rbac_exceptions.RbacOverPermission,
                                    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)
             self.assertRegex(mock_log.error.mock_calls[0][1][0], error_re)
             mock_log.error.reset_mock()
             self._assert_policy_authority_called_with(rules, mock_authority)
@@ -485,21 +491,26 @@
         rules = [
             mock.sentinel.action1, mock.sentinel.action2, mock.sentinel.action3
         ]
+        exp_ecodes = [403, 403, 403]
 
-        @rbac_rv.action(mock.sentinel.service, rules=rules)
+        @rbac_rv.action(mock.sentinel.service, rules=rules,
+                        expected_error_codes=exp_ecodes)
         def test_policy(*args):
             raise exceptions.Forbidden()
 
-        def _do_test(allowed_list):
+        def _do_test(allowed_list, fail_on_action):
             mock_authority.PolicyAuthority.return_value.allowed.\
                 side_effect = allowed_list
             test_policy(self.mock_test_args)
+            mock_log.debug.assert_called_with(
+                "%s: Expecting %d to be raised for policy name: %s",
+                'test_policy', 403, fail_on_action)
             mock_log.error.assert_not_called()
             self._assert_policy_authority_called_with(rules, mock_authority)
 
-        _do_test([True, True, False])
-        _do_test([False, True, True])
-        _do_test([True, False, True])
+        _do_test([True, True, False], mock.sentinel.action3)
+        _do_test([False, True, True], mock.sentinel.action1)
+        _do_test([True, False, True], mock.sentinel.action2)
 
     @mock.patch.object(rbac_rv, 'LOG', autospec=True)
     @mock.patch.object(rbac_rv, 'policy_authority', autospec=True)
@@ -513,7 +524,8 @@
         # NOTE: Avoid mock.sentinel here due to weird sorting with them.
         rules = ['action1', 'action2', 'action3']
 
-        @rbac_rv.action(mock.sentinel.service, rules=rules)
+        @rbac_rv.action(mock.sentinel.service, rules=rules,
+                        expected_error_codes=[403, 403, 403])
         def test_policy(*args):
             raise exceptions.Forbidden()
 
@@ -528,3 +540,136 @@
                                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)
+
+    @mock.patch.object(rbac_rv, 'LOG', autospec=True)
+    @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
+        two of the actions fail and the first action specifies 403,
+        verify that the overall evaluation results in success.
+        """
+
+        rules = [
+            mock.sentinel.action1, mock.sentinel.action2, mock.sentinel.action3
+        ]
+        exp_ecodes = [403, 403, 404]
+
+        @rbac_rv.action(mock.sentinel.service, rules=rules,
+                        expected_error_codes=exp_ecodes)
+        def test_policy(*args):
+            raise exceptions.Forbidden()
+
+        def _do_test(allowed_list, fail_on_action):
+            mock_authority.PolicyAuthority.return_value.allowed.\
+                side_effect = allowed_list
+            test_policy(self.mock_test_args)
+            mock_log.debug.assert_called_with(
+                "%s: Expecting %d to be raised for policy name: %s",
+                'test_policy', 403, fail_on_action)
+            mock_log.error.assert_not_called()
+            self._assert_policy_authority_called_with(rules, mock_authority)
+
+        _do_test([False, True, False], mock.sentinel.action1)
+        _do_test([False, False, True], mock.sentinel.action1)
+
+    @mock.patch.object(rbac_rv, 'LOG', autospec=True)
+    @mock.patch.object(rbac_rv, 'policy_authority', autospec=True)
+    def test_rule_validation_multi_actions_notfound(
+            self, mock_authority, mock_log):
+        """Test that when the expected result is not found because
+        two of the actions fail and the first action specifies 404,
+        verify that the overall evaluation results in success.
+        """
+
+        rules = [
+            mock.sentinel.action1, mock.sentinel.action2,
+            mock.sentinel.action3, mock.sentinel.action4
+        ]
+        exp_ecodes = [403, 404, 403, 403]
+
+        @rbac_rv.action(mock.sentinel.service, rules=rules,
+                        expected_error_codes=exp_ecodes)
+        def test_policy(*args):
+            raise exceptions.NotFound()
+
+        def _do_test(allowed_list, fail_on_action):
+            mock_authority.PolicyAuthority.return_value.allowed.\
+                side_effect = allowed_list
+            test_policy(self.mock_test_args)
+            mock_log.debug.assert_called_with(
+                "%s: Expecting %d to be raised for policy name: %s",
+                'test_policy', 404, fail_on_action)
+            mock_log.error.assert_not_called()
+            self._assert_policy_authority_called_with(rules, mock_authority)
+
+        _do_test([True, False, False, True], mock.sentinel.action2)
+        _do_test([True, False, True, False], mock.sentinel.action2)
+
+    @mock.patch.object(rbac_rv, 'LOG', autospec=True)
+    def test_prepare_multi_policy_allowed_usages(self, mock_log):
+
+        def _do_test(rule, rules, ecode, ecodes, exp_rules, exp_ecodes):
+            rule_list, ec_list = rbac_rv._prepare_multi_policy(rule, rules,
+                                                               ecode, ecodes)
+            self.assertEqual(rule_list, exp_rules)
+            self.assertEqual(ec_list, exp_ecodes)
+
+        # Validate that using deprecated values: rule and expected_error_code
+        # are converted into rules = [rule] and expected_error_codes =
+        # [expected_error_code]
+        _do_test("rule1", None, 403, None, ["rule1"], [403])
+
+        # Validate that rules = [rule] and expected_error_codes defaults to
+        # 403 when no values are provided.
+        _do_test("rule1", None, None, None, ["rule1"], [403])
+
+        # Validate that `len(rules) == len(expected_error_codes)` works when
+        # both == 1.
+        _do_test(None, ["rule1"], None, [403], ["rule1"], [403])
+
+        # Validate that `len(rules) == len(expected_error_codes)` works when
+        # both are > 1.
+        _do_test(None, ["rule1", "rule2"], None, [403, 404],
+                 ["rule1", "rule2"], [403, 404])
+
+        # Validate that when only a default expected_error_code argument is
+        # provided, that default value and other default values (403) are
+        # filled into the expected_error_codes list.
+        # Example:
+        #     @rbac_rv.action(service, rules=[<rule>, <rule>])
+        #     def test_policy(*args):
+        #        ...
+        _do_test(None, ["rule1", "rule2"], 403, None,
+                 ["rule1", "rule2"], [403, 403])
+
+        # Validate that the deprecated values are ignored when new values are
+        # provided.
+        _do_test("rule3", ["rule1", "rule2"], 404, [403, 403],
+                 ["rule1", "rule2"], [403, 403])
+        mock_log.debug.assert_any_call(
+            "The `rules` argument will be used instead of `rule`.")
+        mock_log.debug.assert_any_call(
+            "The `exp_error_codes` argument will be used instead of "
+            "`exp_error_code`.")
+
+    @mock.patch.object(rbac_rv, 'LOG', autospec=True)
+    def test_prepare_multi_policy_disallowed_usages(self, mock_log):
+
+        def _do_test(rule, rules, ecode, ecodes):
+            rule_list, ec_list = rbac_rv._prepare_multi_policy(rule, rules,
+                                                               ecode, ecodes)
+
+        error_re = ("The `expected_error_codes` list is not the same length"
+                    " as the `rules` list.")
+        # When len(rules) > 1 then len(expected_error_codes) must be same len.
+        self.assertRaisesRegex(ValueError, error_re, _do_test,
+                               None, ["rule1", "rule2"], None, [403])
+        # When len(expected_error_codes) > 1 len(rules) must be same len.
+        self.assertRaisesRegex(ValueError, error_re, _do_test,
+                               None, ["rule1"], None, [403, 404])
+        error_re = ("The `rules` list must be provided if using the "
+                    "`expected_error_codes` list.")
+        # When expected_error_codes is provided rules must be as well.
+        self.assertRaisesRegex(ValueError, error_re, _do_test,
+                               None, None, None, [404])
diff --git a/patrole_tempest_plugin/tests/unit/test_rbac_utils.py b/patrole_tempest_plugin/tests/unit/test_rbac_utils.py
index 5e730d3..4937318 100644
--- a/patrole_tempest_plugin/tests/unit/test_rbac_utils.py
+++ b/patrole_tempest_plugin/tests/unit/test_rbac_utils.py
@@ -36,17 +36,15 @@
 
     def test_override_role_with_missing_admin_role(self):
         self.rbac_utils.set_roles('member')
-        error_re = (
-            'Roles defined by `\[patrole\] rbac_test_role` and `\[identity\] '
-            'admin_role` must be defined in the system.')
+        error_re = (".*Following roles were not found: admin. Available "
+                    "roles: member.")
         self.assertRaisesRegex(rbac_exceptions.RbacResourceSetupFailed,
                                error_re, self.rbac_utils.override_role)
 
     def test_override_role_with_missing_rbac_role(self):
         self.rbac_utils.set_roles('admin')
-        error_re = (
-            'Roles defined by `\[patrole\] rbac_test_role` and `\[identity\] '
-            'admin_role` must be defined in the system.')
+        error_re = (".*Following roles were not found: member. Available "
+                    "roles: admin.")
         self.assertRaisesRegex(rbac_exceptions.RbacResourceSetupFailed,
                                error_re, self.rbac_utils.override_role)
 
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 }}'
diff --git a/releasenotes/notes/multi-policy-support-4e5c8b4e9e25ad9d.yaml b/releasenotes/notes/multi-policy-support-4e5c8b4e9e25ad9d.yaml
index 3d192d9..1f33d8f 100644
--- a/releasenotes/notes/multi-policy-support-4e5c8b4e9e25ad9d.yaml
+++ b/releasenotes/notes/multi-policy-support-4e5c8b4e9e25ad9d.yaml
@@ -7,7 +7,25 @@
     expected test result. This allows Patrole to more accurately determine
     whether RBAC is configured correctly, since some API endpoints enforce
     multiple policies.
+
+    Multiple policy support includes the capability to specify multiple
+    expected error codes, as some components may return different error codes
+    for different roles due to checking multiple policy rules. The
+    ``expected_error_codes`` argument has been added to the
+    ``rbac_rule_validation.action`` decorator, which is a list of error codes
+    expected when the corresponding rule in the ``rules`` list is disallowed
+    to perform the API action. For this reason, the error codes in the
+    ``expected_error_codes`` list must appear in the same order as their
+    corresponding rules in the ``rules`` list. For example:
+
+        expected_error_codes[0] is the error code for the rules[0] rule.
+        expected_error_codes[1] is the error code for the rules[1] rule.
+        ...
+
 deprecations:
   - |
     The ``rule`` argument in the ``rbac_rule_validation.action`` decorator has
     been deprecated in favor of ``rules``.
+
+    The ``expected_error_code`` argument in the ``rbac_rule_validation.action``
+    decorator has been deprecated in favor of ``expected_error_codes``.
diff --git a/tox.ini b/tox.ini
index cca09d0..a09822f 100644
--- a/tox.ini
+++ b/tox.ini
@@ -21,16 +21,20 @@
     stestr --test-path ./patrole_tempest_plugin/tests/unit run {posargs}
 
 [testenv:pep8]
+basepython = python3
 commands = flake8 {posargs}
            check-uuid --package patrole_tempest_plugin.tests.api
 
 [testenv:uuidgen]
+basepython = python3
 commands = check-uuid --package patrole_tempest_plugin.tests.api --fix
 
 [testenv:venv]
+basepython = python3
 commands = {posargs}
 
 [testenv:cover]
+basepython = python3
 commands = rm -rf *.pyc
            rm -rf cover
            rm -f .coverage
@@ -46,6 +50,7 @@
                       rm
 
 [testenv:docs]
+basepython = python3
 deps =
   -c{env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt}
   -r{toxinidir}/requirements.txt
@@ -56,6 +61,7 @@
 whitelist_externals = rm
 
 [testenv:releasenotes]
+basepython = python3
 deps =
   -c{env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt}
   -r{toxinidir}/requirements.txt
@@ -66,9 +72,11 @@
 whitelist_externals = rm
 
 [testenv:debug]
+basepython = python3
 commands = oslo_debug_helper -t patrole_tempest_plugin/tests {posargs}
 
 [testenv:genconfig]
+basepython = python3
 commands = oslo-config-generator --config-file etc/config-generator.patrole.conf
 
 [flake8]