Implement RbacUtilsMixin for base RBAC classes

This PS implements a RbacUtilsMixin mixin class in rbac_utils
module. This mixin is useful for doing basic Patrole setup in one
place. The mixin currently handles skipping tests if the flag
`[patrole] enable_rbac` is False and initializing rbac_utils
for each RBAC test.

Following changes have been made:
  * Implementation of RbacUtilsMixin
  * Associated unit tests
  * Refactor base RBAC classes to use the mixin
  * Trivial documentation changes

Change-Id: Ieaf19ccc8ce374b12af4c481a2bddcdbe86dedec
diff --git a/patrole_tempest_plugin/rbac_utils.py b/patrole_tempest_plugin/rbac_utils.py
index 4ef0f80..753c915 100644
--- a/patrole_tempest_plugin/rbac_utils.py
+++ b/patrole_tempest_plugin/rbac_utils.py
@@ -114,7 +114,7 @@
           * admin if `toggle_rbac_role` is False
           * `CONF.patrole.rbac_test_role` if `toggle_rbac_role` is True
 
-        :param test_obj: test object of type tempest.lib.base.BaseTestCase
+        :param test_obj: instance of :py:class:`tempest.test.BaseTestCase`
         :param toggle_rbac_role: role to switch `os_primary` Tempest creds to
         """
         self._override_role(test_obj, toggle_rbac_role)
@@ -122,7 +122,7 @@
     def _override_role(self, test_obj, toggle_rbac_role=False):
         """Private helper for overriding ``os_primary`` Tempest credentials.
 
-        :param test_obj: test object of type tempest.lib.base.BaseTestCase
+        :param test_obj: instance of :py:class:`tempest.test.BaseTestCase`
         :param toggle_rbac_role: Boolean value that controls the role that
             overrides default role of ``os_primary`` credentials.
             * If True: role is set to ``[patrole] rbac_test_role``
@@ -203,6 +203,39 @@
         return False
 
 
+class RbacUtilsMixin(object):
+    """Mixin class to be used alongside an instance of
+    :py:class:`tempest.test.BaseTestCase`.
+
+    Should be used to perform Patrole class setup for a base RBAC class. Child
+    classes should not use this mixin.
+
+    Example::
+
+        class BaseRbacTest(rbac_utils.RbacUtilsMixin, base.BaseV2ComputeTest):
+
+            @classmethod
+            def skip_checks(cls):
+                super(BaseRbacTest, cls).skip_checks()
+                cls.skip_rbac_checks()
+
+            @classmethod
+            def setup_clients(cls):
+                super(BaseRbacTest, cls).setup_clients()
+                cls.setup_rbac_utils()
+    """
+
+    @classmethod
+    def skip_rbac_checks(cls):
+        if not CONF.patrole.enable_rbac:
+            raise cls.skipException(
+                '%s skipped as Patrole testing not enabled.' % cls.__name__)
+
+    @classmethod
+    def setup_rbac_utils(cls):
+        cls.rbac_utils = RbacUtils(cls)
+
+
 def is_admin():
     """Verifies whether the current test role equals the admin role.
 
diff --git a/patrole_tempest_plugin/tests/api/compute/rbac_base.py b/patrole_tempest_plugin/tests/api/compute/rbac_base.py
index 6246446..1bd1cc7 100644
--- a/patrole_tempest_plugin/tests/api/compute/rbac_base.py
+++ b/patrole_tempest_plugin/tests/api/compute/rbac_base.py
@@ -21,20 +21,18 @@
 CONF = config.CONF
 
 
-class BaseV2ComputeRbacTest(compute_base.BaseV2ComputeTest):
+class BaseV2ComputeRbacTest(rbac_utils.RbacUtilsMixin,
+                            compute_base.BaseV2ComputeTest):
 
     @classmethod
     def skip_checks(cls):
         super(BaseV2ComputeRbacTest, cls).skip_checks()
-        if not CONF.patrole.enable_rbac:
-            raise cls.skipException(
-                '%s skipped as RBAC testing not enabled' % cls.__name__)
+        cls.skip_rbac_checks()
 
     @classmethod
     def setup_clients(cls):
         super(BaseV2ComputeRbacTest, cls).setup_clients()
-        cls.rbac_utils = rbac_utils.RbacUtils(cls)
-
+        cls.setup_rbac_utils()
         cls.hosts_client = cls.os_primary.hosts_client
         cls.tenant_usages_client = cls.os_primary.tenant_usages_client
 
diff --git a/patrole_tempest_plugin/tests/api/identity/rbac_base.py b/patrole_tempest_plugin/tests/api/identity/rbac_base.py
index a99365d..63f6ff8 100644
--- a/patrole_tempest_plugin/tests/api/identity/rbac_base.py
+++ b/patrole_tempest_plugin/tests/api/identity/rbac_base.py
@@ -26,19 +26,18 @@
 LOG = logging.getLogger(__name__)
 
 
-class BaseIdentityRbacTest(base.BaseIdentityTest):
+class BaseIdentityRbacTest(rbac_utils.RbacUtilsMixin,
+                           base.BaseIdentityTest):
 
     @classmethod
     def skip_checks(cls):
         super(BaseIdentityRbacTest, cls).skip_checks()
-        if not CONF.patrole.enable_rbac:
-            raise cls.skipException(
-                "%s skipped as RBAC testing not enabled" % cls.__name__)
+        cls.skip_rbac_checks()
 
     @classmethod
     def setup_clients(cls):
         super(BaseIdentityRbacTest, cls).setup_clients()
-        cls.rbac_utils = rbac_utils.RbacUtils(cls)
+        cls.setup_rbac_utils()
 
     @classmethod
     def setup_test_endpoint(cls, service=None):
diff --git a/patrole_tempest_plugin/tests/api/image/rbac_base.py b/patrole_tempest_plugin/tests/api/image/rbac_base.py
index ed69c3d..954790d 100644
--- a/patrole_tempest_plugin/tests/api/image/rbac_base.py
+++ b/patrole_tempest_plugin/tests/api/image/rbac_base.py
@@ -19,16 +19,15 @@
 CONF = config.CONF
 
 
-class BaseV2ImageRbacTest(image_base.BaseV2ImageTest):
+class BaseV2ImageRbacTest(rbac_utils.RbacUtilsMixin,
+                          image_base.BaseV2ImageTest):
 
     @classmethod
     def skip_checks(cls):
         super(BaseV2ImageRbacTest, cls).skip_checks()
-        if not CONF.patrole.enable_rbac:
-            raise cls.skipException(
-                "%s skipped as RBAC testing not enabled" % cls.__name__)
+        cls.skip_rbac_checks()
 
     @classmethod
     def setup_clients(cls):
         super(BaseV2ImageRbacTest, cls).setup_clients()
-        cls.rbac_utils = rbac_utils.RbacUtils(cls)
+        cls.setup_rbac_utils()
diff --git a/patrole_tempest_plugin/tests/api/network/rbac_base.py b/patrole_tempest_plugin/tests/api/network/rbac_base.py
index b495098..3065c13 100644
--- a/patrole_tempest_plugin/tests/api/network/rbac_base.py
+++ b/patrole_tempest_plugin/tests/api/network/rbac_base.py
@@ -21,16 +21,15 @@
 CONF = config.CONF
 
 
-class BaseNetworkRbacTest(network_base.BaseNetworkTest):
+class BaseNetworkRbacTest(rbac_utils.RbacUtilsMixin,
+                          network_base.BaseNetworkTest):
 
     @classmethod
     def skip_checks(cls):
         super(BaseNetworkRbacTest, cls).skip_checks()
-        if not CONF.patrole.enable_rbac:
-            raise cls.skipException(
-                "%s skipped as RBAC testing not enabled" % cls.__name__)
+        cls.skip_rbac_checks()
 
     @classmethod
     def setup_clients(cls):
         super(BaseNetworkRbacTest, cls).setup_clients()
-        cls.rbac_utils = rbac_utils.RbacUtils(cls)
+        cls.setup_rbac_utils()
diff --git a/patrole_tempest_plugin/tests/api/volume/rbac_base.py b/patrole_tempest_plugin/tests/api/volume/rbac_base.py
index 7e2ebad..798f311 100644
--- a/patrole_tempest_plugin/tests/api/volume/rbac_base.py
+++ b/patrole_tempest_plugin/tests/api/volume/rbac_base.py
@@ -21,7 +21,8 @@
 CONF = config.CONF
 
 
-class BaseVolumeRbacTest(vol_base.BaseVolumeTest):
+class BaseVolumeRbacTest(rbac_utils.RbacUtilsMixin,
+                         vol_base.BaseVolumeTest):
     # NOTE(felipemonteiro): Patrole currently only tests the v3 Cinder API
     # because it is the current API and because policy enforcement does not
     # change between API major versions. So, it is not necessary to specify
@@ -32,15 +33,12 @@
     @classmethod
     def skip_checks(cls):
         super(BaseVolumeRbacTest, cls).skip_checks()
-        if not CONF.patrole.enable_rbac:
-            raise cls.skipException(
-                "%s skipped as RBAC testing not enabled" % cls.__name__)
+        cls.skip_rbac_checks()
 
     @classmethod
     def setup_clients(cls):
         super(BaseVolumeRbacTest, cls).setup_clients()
-        cls.rbac_utils = rbac_utils.RbacUtils(cls)
-
+        cls.setup_rbac_utils()
         cls.volume_hosts_client = cls.os_primary.volume_hosts_v2_client
         cls.volume_types_client = cls.os_primary.volume_types_v2_client
         cls.groups_client = cls.os_primary.groups_v3_client
diff --git a/patrole_tempest_plugin/tests/unit/test_rbac_utils.py b/patrole_tempest_plugin/tests/unit/test_rbac_utils.py
index 0d75c3e..55db501 100644
--- a/patrole_tempest_plugin/tests/unit/test_rbac_utils.py
+++ b/patrole_tempest_plugin/tests/unit/test_rbac_utils.py
@@ -17,6 +17,7 @@
 import testtools
 
 from tempest.lib import exceptions as lib_exc
+from tempest import test
 from tempest.tests import base
 
 from patrole_tempest_plugin import rbac_exceptions
@@ -199,3 +200,60 @@
             _do_test()
         mock_override_role.assert_called_once_with(_rbac_utils, test_obj,
                                                    False)
+
+
+class RBACUtilsMixinTest(base.TestCase):
+
+    def setUp(self):
+        super(RBACUtilsMixinTest, self).setUp()
+
+        class FakeRbacTest(rbac_utils.RbacUtilsMixin, test.BaseTestCase):
+
+            @classmethod
+            def skip_checks(cls):
+                super(FakeRbacTest, cls).skip_checks()
+                cls.skip_rbac_checks()
+
+            @classmethod
+            def setup_clients(cls):
+                super(FakeRbacTest, cls).setup_clients()
+                cls.setup_rbac_utils()
+
+            def runTest(self):
+                pass
+
+        self.parent_class = FakeRbacTest
+
+    def test_setup_rbac_utils(self):
+        """Validate that the child class has the `rbac_utils` attribute after
+        running parent class's `cls.setup_rbac_utils`.
+        """
+        class ChildRbacTest(self.parent_class):
+            pass
+
+        child_test = ChildRbacTest()
+
+        with mock.patch.object(rbac_utils.RbacUtils, '__init__',
+                               lambda *args: None):
+            child_test.setUpClass()
+
+        self.assertTrue(hasattr(child_test, 'rbac_utils'))
+        self.assertIsInstance(child_test.rbac_utils, rbac_utils.RbacUtils)
+
+    def test_skip_rbac_checks(self):
+        """Validate that the child class is skipped if `[patrole] enable_rbac`
+        is False and that the child class's name is in the skip message.
+        """
+        self.useFixture(patrole_fixtures.ConfPatcher(enable_rbac=False,
+                                                     group='patrole'))
+
+        class ChildRbacTest(self.parent_class):
+            pass
+
+        child_test = ChildRbacTest()
+
+        with testtools.ExpectedException(
+                testtools.TestCase.skipException,
+                value_re=('%s skipped as Patrole testing not enabled.'
+                          % ChildRbacTest.__name__)):
+            child_test.setUpClass()