Merge "Helper for validating RBAC list actions"
diff --git a/.gitignore b/.gitignore
index 350e0da..324269e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -45,6 +45,7 @@
 # Sphinx
 doc/build
 doc/source/_static/patrole.conf.sample
+doc/source/framework/code/
 
 # pbr generates these
 AUTHORS
diff --git a/.zuul.yaml b/.zuul.yaml
index 085e775..bed19c4 100644
--- a/.zuul.yaml
+++ b/.zuul.yaml
@@ -16,8 +16,8 @@
       - ^.*\.rst$
       - ^doc/.*
       - ^etc/.*$
-      - ^patrole/patrole_tempest_plugin/tests/unit/.*$
-      - ^patrole/patrole_tempest_plugin/hacking/.*$
+      - ^patrole_tempest_plugin/tests/unit/.*$
+      - ^patrole_tempest_plugin/hacking/.*$
       - ^releasenotes/.*
       - ^setup.cfg$
     vars:
@@ -77,10 +77,7 @@
     parent: patrole-base
     description: Patrole job for member role.
     # This currently works from stable/pike onward.
-    branches:
-      - master
-      - stable/queens
-      - stable/pike
+    branches: ^(?!stable/ocata).*$
     vars:
       devstack_localrc:
         RBAC_TEST_ROLES: member
diff --git a/HACKING.rst b/HACKING.rst
index 9992017..3379292 100644
--- a/HACKING.rst
+++ b/HACKING.rst
@@ -43,6 +43,23 @@
 
 .. _extension test class: https://github.com/openstack/patrole/tree/master/patrole_tempest_plugin/tests/api/network#neutron-extension-rbac-tests
 
+Supported OpenStack Components
+------------------------------
+
+Patrole only offers **in-tree** integration testing coverage for the following
+components:
+
+* Cinder
+* Glance
+* Keystone
+* Neutron
+* Nova
+
+Patrole currently has no stable library, so reliance upon Patrole's framework
+for external RBAC testing should be done with caution. Nonetheless, even when
+Patrole has a stable library, it will only offer in-tree RBAC testing for
+the components listed above.
+
 Role Overriding
 ---------------
 
@@ -121,3 +138,34 @@
 policies also applies to Patrole.
 
 .. _Tempest logic: https://docs.openstack.org/tempest/latest/HACKING.html#new-tests-for-existing-features
+
+
+Black Box vs. White Box Testing
+-------------------------------
+
+Tempest is a `black box testing framework`_, meaning that it is concerned with
+testing public API endpoints and doesn't concern itself with testing internal
+implementation details. Patrole, as a Tempest plugin, also falls underneath
+the category of black box testing. However, even with policy in code
+documentation, some degree of white box testing is required in order to
+correctly write RBAC tests.
+
+This is because :ref:`policy-in-code` documentation, while useful in many
+respects, is usually quite brief and its main purpose is to help operators
+understand how to customize policy configuration rather than to help
+developers understand complex policy authorization work flows. For example,
+policy in code documentation doesn't make deriving
+:ref:`multiple policies <multiple-policies>` easy. Such documentation also
+doesn't usually mention that a specific parameter needs to be set, or that a
+particular microversion must be enabled, or that a particular set of
+prerequisite API or policy actions must be executed, in order for the policy
+under test to be enforced by the server. This means that test writers must
+account for the internal RBAC implementation in API code in order to correctly
+understand the complete RBAC work flow within an API.
+
+Besides, as mentioned :ref:`elsewhere <design-principles>` in this
+documentation, not all services currently implement policy in code, making
+some degree of white box testing a "necessary evil" for writing robust RBAC
+tests.
+
+.. _black box testing framework: https://docs.openstack.org/tempest/latest/HACKING.html#negative-tests
diff --git a/README.rst b/README.rst
index 31cd3b7..713756a 100644
--- a/README.rst
+++ b/README.rst
@@ -18,9 +18,6 @@
 allowing deployments to verify that only intended roles have access to those
 APIs.
 
-Patrole currently offers testing for the following OpenStack services: Nova,
-Neutron, Glance, Cinder and Keystone.
-
 Patrole is currently undergoing heavy development. As more projects move
 toward policy in code, Patrole will align its testing with the appropriate
 documentation.
@@ -202,8 +199,14 @@
   **admin** and **member** roles. However, other services may use entirely
   different roles or role combinations.
 
-For more information about the member role and its nomenclature,
-please see: `<https://ask.openstack.org/en/question/4759/member-vs-_member_/>`__.
+For more information about RBAC, reference the `rbac-overview`_
+documentation page.
+
+For information regarding which projects Patrole offers RBAC testing for,
+reference the `HACKING`_ documentation page.
+
+.. _rbac-overview: https://docs.openstack.org/patrole/latest/rbac-overview.html
+.. _HACKING: https://docs.openstack.org/patrole/latest/HACKING.html#supported-openstack-components
 
 Unit Tests
 ----------
diff --git a/doc/requirements.txt b/doc/requirements.txt
index 012efb2..67530ff 100644
--- a/doc/requirements.txt
+++ b/doc/requirements.txt
@@ -4,3 +4,4 @@
 sphinx!=1.6.6,!=1.6.7,>=1.6.2 # BSD
 openstackdocstheme>=1.18.1 # Apache-2.0
 reno>=2.5.0 # Apache-2.0
+sphinxcontrib-apidoc>=0.2.0  # BSD
diff --git a/doc/source/conf.py b/doc/source/conf.py
index 88c1bea..45d8021 100755
--- a/doc/source/conf.py
+++ b/doc/source/conf.py
@@ -15,7 +15,13 @@
 import os
 import sys
 
-sys.path.insert(0, os.path.abspath('../..'))
+# If extensions (or modules to document with autodoc) are in another directory,
+# add these directories to sys.path here. If the directory is relative to the
+# documentation root, use os.path.abspath to make it absolute, like shown here.
+sys.path.insert(0, os.path.abspath('../../'))
+sys.path.insert(0, os.path.abspath('../'))
+sys.path.insert(0, os.path.abspath('./'))
+
 # -- General configuration ----------------------------------------------------
 
 # Add any Sphinx extension module names here, as strings. They can be
@@ -26,8 +32,23 @@
     'sphinx.ext.viewcode',
     'openstackdocstheme',
     'oslo_config.sphinxconfiggen',
+    'sphinxcontrib.apidoc',
 ]
 
+# sphinxcontrib.apidoc options
+apidoc_module_dir = '../../patrole_tempest_plugin'
+apidoc_output_dir = 'framework/code'
+apidoc_excluded_paths = [
+    'hacking',
+    'hacking/*',
+    'tests',
+    'tests/*',
+    'config.py',
+    'plugin.py',
+    'version.py'
+]
+apidoc_separate_modules = True
+
 config_generator_config_file = '../../etc/config-generator.patrole.conf'
 sample_config_basename = '_static/patrole'
 
diff --git a/doc/source/framework/policy_authority.rst b/doc/source/framework/policy_authority.rst
index 37b698c..f039692 100644
--- a/doc/source/framework/policy_authority.rst
+++ b/doc/source/framework/policy_authority.rst
@@ -57,7 +57,4 @@
 Implementation
 --------------
 
-.. automodule:: patrole_tempest_plugin.policy_authority
-   :members:
-   :undoc-members:
-   :special-members:
+:py:mod:`Policy Authority Module <patrole_tempest_plugin.policy_authority>`
diff --git a/doc/source/framework/rbac_authority.rst b/doc/source/framework/rbac_authority.rst
index 40f2a8d..7ffe24f 100644
--- a/doc/source/framework/rbac_authority.rst
+++ b/doc/source/framework/rbac_authority.rst
@@ -32,7 +32,4 @@
 Implementation
 --------------
 
-.. automodule:: patrole_tempest_plugin.rbac_authority
-   :members:
-   :undoc-members:
-   :special-members:
+:py:mod:`RBAC Authority Module <patrole_tempest_plugin.rbac_authority>`
diff --git a/doc/source/framework/rbac_utils.rst b/doc/source/framework/rbac_utils.rst
index b13a4a3..f7cb182 100644
--- a/doc/source/framework/rbac_utils.rst
+++ b/doc/source/framework/rbac_utils.rst
@@ -26,10 +26,7 @@
 Implementation
 --------------
 
-.. automodule:: patrole_tempest_plugin.rbac_utils
-   :members:
-   :private-members:
-   :special-members:
+:py:mod:`RBAC Utils Module <patrole_tempest_plugin.rbac_utils>`
 
 .. _Tempest credentials: https://docs.openstack.org/tempest/latest/library/credential_providers.html
 .. _dynamic credentials: https://docs.openstack.org/tempest/latest/configuration.html#dynamic-credentials
diff --git a/doc/source/framework/rbac_validation.rst b/doc/source/framework/rbac_validation.rst
index 6cd1534..460c08c 100644
--- a/doc/source/framework/rbac_validation.rst
+++ b/doc/source/framework/rbac_validation.rst
@@ -14,7 +14,4 @@
 Implementation
 --------------
 
-.. automodule:: patrole_tempest_plugin.rbac_rule_validation
-   :members:
-   :private-members:
-   :special-members:
+:py:mod:`RBAC Rule Validation Module <patrole_tempest_plugin.rbac_rule_validation>`
diff --git a/doc/source/framework/requirements_authority.rst b/doc/source/framework/requirements_authority.rst
index cf7c51c..daed319 100644
--- a/doc/source/framework/requirements_authority.rst
+++ b/doc/source/framework/requirements_authority.rst
@@ -153,7 +153,4 @@
 Implementation
 --------------
 
-.. automodule:: patrole_tempest_plugin.requirements_authority
-   :members:
-   :undoc-members:
-   :special-members:
+:py:mod:`Requirements Authority Module <patrole_tempest_plugin.requirements_authority>`
diff --git a/doc/source/index.rst b/doc/source/index.rst
index a9dcdc0..816908a 100644
--- a/doc/source/index.rst
+++ b/doc/source/index.rst
@@ -61,7 +61,7 @@
 ---------
 
 .. toctree::
-   :maxdepth: 2
+   :maxdepth: 3
 
    framework/overview
    framework/rbac_validation
@@ -69,6 +69,7 @@
    framework/policy_authority
    framework/requirements_authority
    framework/rbac_utils
+   framework/code/modules
 
 Indices and tables
 ==================
diff --git a/doc/source/rbac-overview.rst b/doc/source/rbac-overview.rst
index acfd66f..cc47f75 100644
--- a/doc/source/rbac-overview.rst
+++ b/doc/source/rbac-overview.rst
@@ -1,3 +1,5 @@
+.. _rbac-overview:
+
 ==================================
 Role-Based Access Control Overview
 ==================================
diff --git a/patrole_tempest_plugin/config.py b/patrole_tempest_plugin/config.py
index 90d5fba..62337f7 100644
--- a/patrole_tempest_plugin/config.py
+++ b/patrole_tempest_plugin/config.py
@@ -117,7 +117,7 @@
     cfg.StrOpt('report_log_path',
                default='.',
                help="Path (relative or absolute) where the output from "
-                    "'enable_reporting' is logged. This is combined with"
+                    "'enable_reporting' is logged. This is combined with "
                     "report_log_name to generate the full path."),
 ]
 
diff --git a/patrole_tempest_plugin/tests/api/network/rbac_base.py b/patrole_tempest_plugin/tests/api/network/rbac_base.py
index 347651d..dc0ce7f 100644
--- a/patrole_tempest_plugin/tests/api/network/rbac_base.py
+++ b/patrole_tempest_plugin/tests/api/network/rbac_base.py
@@ -13,7 +13,10 @@
 #    License for the specific language governing permissions and limitations
 #    under the License.
 
+from oslo_serialization import jsonutils as json
+
 from tempest.api.network import base as network_base
+from tempest.lib.common.utils import test_utils
 
 from patrole_tempest_plugin import rbac_utils
 
@@ -72,3 +75,13 @@
             cls.ntp_client = neutron_tempest_manager.network_client
 
         return manager
+
+    @classmethod
+    def create_service_profile(cls):
+        service_profile = cls.ntp_client.create_service_profile(
+            metainfo=json.dumps({'foo': 'bar'}))
+        service_profile_id = service_profile["service_profile"]["id"]
+        cls.addClassResourceCleanup(
+            test_utils.call_and_ignore_notfound_exc,
+            cls.ntp_client.delete_service_profile, service_profile_id)
+        return service_profile_id
diff --git a/patrole_tempest_plugin/tests/api/network/test_flavor_service_profile_rbac.py b/patrole_tempest_plugin/tests/api/network/test_flavor_service_profile_rbac.py
new file mode 100644
index 0000000..db0b8f1
--- /dev/null
+++ b/patrole_tempest_plugin/tests/api/network/test_flavor_service_profile_rbac.py
@@ -0,0 +1,77 @@
+# Copyright 2018 AT&T Corporation.
+# All Rights Reserved.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+from tempest.lib.common.utils import test_utils
+from tempest.lib import decorators
+
+from patrole_tempest_plugin import rbac_rule_validation
+from patrole_tempest_plugin.tests.api.network import rbac_base as base
+
+
+class FlavorsServiceProfileExtRbacTest(base.BaseNetworkExtRbacTest):
+    @classmethod
+    def resource_setup(cls):
+        super(FlavorsServiceProfileExtRbacTest, cls).resource_setup()
+        providers = cls.ntp_client.list_service_providers()
+        if not providers["service_providers"]:
+            raise cls.skipException("No service_providers available.")
+        cls.service_type = providers["service_providers"][0]["service_type"]
+
+        cls.flavor_id = cls.create_flavor()
+        cls.service_profile_id = cls.create_service_profile()
+
+    @classmethod
+    def create_flavor(cls):
+        flavor = cls.ntp_client.create_flavor(service_type=cls.service_type)
+        flavor_id = flavor["flavor"]["id"]
+        cls.addClassResourceCleanup(
+            test_utils.call_and_ignore_notfound_exc,
+            cls.ntp_client.delete_flavor, flavor_id)
+        return flavor_id
+
+    def create_flavor_service_profile(self, flavor_id, service_profile_id):
+        self.ntp_client.create_flavor_service_profile(
+            flavor_id, service_profile_id)
+        self.addCleanup(
+            test_utils.call_and_ignore_notfound_exc,
+            self.ntp_client.delete_flavor_service_profile,
+            flavor_id, service_profile_id)
+
+    @decorators.idempotent_id('aa84b4c5-0dd6-4c34-aa81-3a76507f9b81')
+    @rbac_rule_validation.action(service="neutron",
+                                 rules=["create_flavor_service_profile"])
+    def test_create_flavor_service_profile(self):
+        """Create flavor_service_profile.
+
+        RBAC test for the neutron "create_flavor_service_profile" policy
+        """
+        with self.rbac_utils.override_role(self):
+            self.create_flavor_service_profile(self.flavor_id,
+                                               self.service_profile_id)
+
+    @decorators.idempotent_id('3b680d9e-946a-4670-ab7f-0e4576675833')
+    @rbac_rule_validation.action(service="neutron",
+                                 rules=["delete_flavor_service_profile"])
+    def test_delete_flavor_service_profile(self):
+        """Delete flavor_service_profile.
+
+        RBAC test for the neutron "delete_flavor_service_profile" policy
+        """
+        self.create_flavor_service_profile(self.flavor_id,
+                                           self.service_profile_id)
+
+        with self.rbac_utils.override_role(self):
+            self.ntp_client.delete_flavor_service_profile(
+                self.flavor_id, self.service_profile_id)
diff --git a/patrole_tempest_plugin/tests/api/network/test_flavors_rbac.py b/patrole_tempest_plugin/tests/api/network/test_flavors_rbac.py
index dea95ba..76c0db3 100644
--- a/patrole_tempest_plugin/tests/api/network/test_flavors_rbac.py
+++ b/patrole_tempest_plugin/tests/api/network/test_flavors_rbac.py
@@ -13,8 +13,6 @@
 #    License for the specific language governing permissions and limitations
 #    under the License.
 
-from oslo_serialization import jsonutils as json
-
 from tempest.lib.common.utils import data_utils
 from tempest.lib.common.utils import test_utils
 from tempest.lib import decorators
@@ -118,72 +116,3 @@
 
         with self.rbac_utils.override_role(self):
             self.ntp_client.list_flavors()
-
-
-class FlavorsServiceProfileExtRbacTest(base.BaseNetworkExtRbacTest):
-    @classmethod
-    def resource_setup(cls):
-        super(FlavorsServiceProfileExtRbacTest, cls).resource_setup()
-        providers = cls.ntp_client.list_service_providers()
-        if not providers["service_providers"]:
-            raise cls.skipException("No service_providers available.")
-        cls.service_type = providers["service_providers"][0]["service_type"]
-
-        cls.flavor_id = cls.create_flavor()
-        cls.service_profile_id = cls.create_service_profile()
-
-    @classmethod
-    def create_flavor(cls):
-        flavor = cls.ntp_client.create_flavor(service_type=cls.service_type)
-        flavor_id = flavor["flavor"]["id"]
-        cls.addClassResourceCleanup(
-            test_utils.call_and_ignore_notfound_exc,
-            cls.ntp_client.delete_flavor, flavor_id)
-        return flavor_id
-
-    @classmethod
-    def create_service_profile(cls):
-        service_profile = cls.ntp_client.create_service_profile(
-            metainfo=json.dumps({'foo': 'bar'}))
-        service_profile_id = service_profile["service_profile"]["id"]
-        cls.addClassResourceCleanup(
-            test_utils.call_and_ignore_notfound_exc,
-            cls.ntp_client.delete_service_profile, service_profile_id)
-        return service_profile_id
-
-    def create_flavor_service_profile(self, flavor_id, service_profile_id):
-        self.ntp_client.create_flavor_service_profile(
-            flavor_id, service_profile_id)
-        self.addCleanup(
-            test_utils.call_and_ignore_notfound_exc,
-            self.ntp_client.delete_flavor_service_profile,
-            flavor_id, service_profile_id)
-
-    @decorators.idempotent_id('aa84b4c5-0dd6-4c34-aa81-3a76507f9b81')
-    @rbac_rule_validation.action(service="neutron",
-                                 rules=["create_flavor_service_profile"])
-    def test_create_flavor_service_profile(self):
-        """Create flavor_service_profile.
-
-        RBAC test for the neutron "create_flavor_service_profile" policy
-        """
-        with self.rbac_utils.override_role(self):
-            self.create_flavor_service_profile(self.flavor_id,
-                                               self.service_profile_id)
-
-    @decorators.idempotent_id('3b680d9e-946a-4670-ab7f-0e4576675833')
-    @rbac_rule_validation.action(service="neutron",
-                                 rules=["get_flavor_service_profile",
-                                        "delete_flavor_service_profile"],
-                                 expected_error_codes=[404, 403])
-    def test_delete_flavor_service_profile(self):
-        """Delete flavor_service_profile.
-
-        RBAC test for the neutron "delete_flavor_service_profile" policy
-        """
-        self.create_flavor_service_profile(self.flavor_id,
-                                           self.service_profile_id)
-
-        with self.rbac_utils.override_role(self):
-            self.ntp_client.delete_flavor_service_profile(
-                self.flavor_id, self.service_profile_id)
diff --git a/patrole_tempest_plugin/tests/api/network/test_service_profile_rbac.py b/patrole_tempest_plugin/tests/api/network/test_service_profile_rbac.py
new file mode 100644
index 0000000..9e82835
--- /dev/null
+++ b/patrole_tempest_plugin/tests/api/network/test_service_profile_rbac.py
@@ -0,0 +1,73 @@
+# Copyright 2018 AT&T Corporation.
+# All Rights Reserved.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License"); you may
+#    not use this file except in compliance with the License. You may obtain
+#    a copy of the License at
+#
+#         http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+#    License for the specific language governing permissions and limitations
+#    under the License.
+
+from tempest.lib import decorators
+
+from patrole_tempest_plugin import rbac_rule_validation
+from patrole_tempest_plugin.tests.api.network import rbac_base as base
+
+
+class ServiceProfileExtRbacTest(base.BaseNetworkExtRbacTest):
+    @decorators.idempotent_id('6ce76efa-7400-44c1-80ec-58f79b1d89ca')
+    @rbac_rule_validation.action(service="neutron",
+                                 rules=["create_service_profile"])
+    def test_create_service_profile(self):
+        """Create service profile
+
+        RBAC test for the neutron "create_service_profile" policy
+        """
+        with self.rbac_utils.override_role(self):
+            self.create_service_profile()
+
+    @decorators.idempotent_id('e4c473b7-3ae9-4a2e-8cac-848f7b01187d')
+    @rbac_rule_validation.action(service="neutron",
+                                 rules=["get_service_profile"],
+                                 expected_error_codes=[404])
+    def test_show_service_profile(self):
+        """Show service profile
+
+        RBAC test for the neutron "get_service_profile" policy
+        """
+        profile_id = self.create_service_profile()
+        with self.rbac_utils.override_role(self):
+            self.ntp_client.show_service_profile(profile_id)
+
+    @decorators.idempotent_id('a3dd719d-4cd3-40cc-b4f1-5642e2717adf')
+    @rbac_rule_validation.action(service="neutron",
+                                 rules=["get_service_profile",
+                                        "update_service_profile"],
+                                 expected_error_codes=[404, 403])
+    def test_update_service_profile(self):
+        """Update service profile
+
+        RBAC test for the neutron "update_service_profile" policy
+        """
+        profile_id = self.create_service_profile()
+        with self.rbac_utils.override_role(self):
+            self.ntp_client.update_service_profile(profile_id, enabled=False)
+
+    @decorators.idempotent_id('926b60c2-04fe-4339-aa44-bf27121392e8')
+    @rbac_rule_validation.action(service="neutron",
+                                 rules=["get_service_profile",
+                                        "delete_service_profile"],
+                                 expected_error_codes=[404, 403])
+    def test_delete_service_profile(self):
+        """Delete service profile
+
+        RBAC test for the neutron "delete_service_profile" policy
+        """
+        profile_id = self.create_service_profile()
+        with self.rbac_utils.override_role(self):
+            self.ntp_client.delete_service_profile(profile_id)
diff --git a/patrole_tempest_plugin/tests/unit/test_rbac_rule_validation.py b/patrole_tempest_plugin/tests/unit/test_rbac_rule_validation.py
index 9e547b8..73a34fc 100644
--- a/patrole_tempest_plugin/tests/unit/test_rbac_rule_validation.py
+++ b/patrole_tempest_plugin/tests/unit/test_rbac_rule_validation.py
@@ -641,6 +641,7 @@
         m_authority.allowed.assert_has_calls([
             mock.call(rule, CONF.patrole.rbac_test_roles) for rule in rules
         ])
+        m_authority.allowed.reset_mock()
 
     @mock.patch.object(rbac_rv, 'policy_authority', autospec=True)
     def test_rule_validation_multi_policy_have_permission_success(
@@ -826,6 +827,44 @@
         _do_test([True, False, False, True], 'mock.sentinel.action2')
         _do_test([True, False, True, False], 'mock.sentinel.action2')
 
+    @mock.patch.object(rbac_rv, 'LOG', autospec=True)
+    @mock.patch.object(rbac_rv, 'policy_authority', autospec=True)
+    def test_rule_validation_multi_policy_defaults_to_correct_error_codes(
+            self, mock_authority, mock_log):
+        """Test omission of expected_error_codes defaults to [403] * len(rules)
+        """
+        mock_authority.PolicyAuthority.return_value.allowed.\
+            return_value = False
+        expected_log = "%s: Expecting %d to be raised for policy name: %s"
+
+        # Validate with single rule => expected_error_codes == [403].
+        rules = [mock.sentinel.action1]
+
+        @rbac_rv.action(mock.sentinel.service, rules=rules)
+        def test_policy(*args):
+            raise exceptions.Forbidden()
+
+        test_policy(self.mock_test_args)
+        self._assert_policy_authority_called_with(rules, mock_authority)
+        # Assert that 403 is expected.
+        mock_calls = [x[1] for x in mock_log.debug.mock_calls]
+        self.assertTrue(
+            any([(expected_log, 'test_policy', 403, rules[0]) in mock_calls]))
+
+        # Validate with multiple rules => expected_error_codes == [403, 403].
+        rules = [mock.sentinel.action1, mock.sentinel.action2]
+
+        @rbac_rv.action(mock.sentinel.service, rules=rules)
+        def test_policy(*args):
+            raise exceptions.Forbidden()
+
+        test_policy(self.mock_test_args)
+        self._assert_policy_authority_called_with(rules, mock_authority)
+        # Assert that 403 is expected.
+        mock_calls = [x[1] for x in mock_log.debug.mock_calls]
+        self.assertTrue(
+            any([(expected_log, 'test_policy', 403, rules[0]) in mock_calls]))
+
     def test_prepare_multi_policy_allowed_usages(self):
 
         def _do_test(rules, ecodes, exp_rules, exp_ecodes):
diff --git a/tox.ini b/tox.ini
index bc829d2..20a7779 100644
--- a/tox.ini
+++ b/tox.ini
@@ -60,7 +60,7 @@
   -r{toxinidir}/requirements.txt
   -r{toxinidir}/doc/requirements.txt
 commands =
-  rm -rf doc/build
+  rm -rf doc/build doc/source/framework/code
   sphinx-build -W -b html doc/source doc/build/html
 whitelist_externals = rm