Multi role RBAC validation

This patchset replaces ``CONF.patrole.rbac_test_role`` with
``CONF.patrole.rbac_test_roles``, where instead of single role
we can specify list of roles to be assigned to test user.

Change-Id: Ia68bcbdbb523dfe7c4abd6107fb4c426a566ae9d
diff --git a/patrole_tempest_plugin/rbac_utils.py b/patrole_tempest_plugin/rbac_utils.py
index b7ac8d9..33955c3 100644
--- a/patrole_tempest_plugin/rbac_utils.py
+++ b/patrole_tempest_plugin/rbac_utils.py
@@ -40,7 +40,7 @@
     up, and primary credentials, needed to perform the API call which does
     policy enforcement. The primary credentials always cycle between roles
     defined by ``CONF.identity.admin_role`` and
-    ``CONF.patrole.rbac_test_role``.
+    ``CONF.patrole.rbac_test_roles``.
     """
 
     def __init__(self, test_obj):
@@ -58,10 +58,15 @@
                 "Patrole role overriding only supports v3 identity API.")
 
         self.admin_roles_client = admin_roles_client
+
+        self.user_id = test_obj.os_primary.credentials.user_id
+        self.project_id = test_obj.os_primary.credentials.tenant_id
+
+        # Change default role to admin
         self._override_role(test_obj, False)
 
     admin_role_id = None
-    rbac_role_id = None
+    rbac_role_ids = None
 
     @contextmanager
     def override_role(self, test_obj):
@@ -69,7 +74,7 @@
 
         Temporarily change the role used by ``os_primary`` credentials to:
 
-        * ``[patrole] rbac_test_role`` before test execution
+        * ``[patrole] rbac_test_roles`` before test execution
         * ``[identity] admin_role`` after test execution
 
         Automatically switches to admin role after test execution.
@@ -122,25 +127,21 @@
             * If True: role is set to ``[patrole] rbac_test_role``
             * If False: role is set to ``[identity] admin_role``
         """
-        self.user_id = test_obj.os_primary.credentials.user_id
-        self.project_id = test_obj.os_primary.credentials.tenant_id
-        self.token = test_obj.os_primary.auth_provider.get_token()
-
         LOG.debug('Overriding role to: %s.', toggle_rbac_role)
-        role_already_present = False
+        roles_already_present = False
 
         try:
-            if not all([self.admin_role_id, self.rbac_role_id]):
+            if not all([self.admin_role_id, self.rbac_role_ids]):
                 self._get_roles_by_name()
 
-            target_role = (
-                self.rbac_role_id if toggle_rbac_role else self.admin_role_id)
-            role_already_present = self._list_and_clear_user_roles_on_project(
-                target_role)
+            target_roles = (self.rbac_role_ids
+                            if toggle_rbac_role else [self.admin_role_id])
+            roles_already_present = self._list_and_clear_user_roles_on_project(
+                target_roles)
 
             # Do not override roles if `target_role` already exists.
-            if not role_already_present:
-                self._create_user_role_on_project(target_role)
+            if not roles_already_present:
+                self._create_user_role_on_project(target_roles)
         except Exception as exp:
             with excutils.save_and_reraise_exception():
                 LOG.exception(exp)
@@ -152,8 +153,8 @@
             # passing the second boundary before attempting to authenticate.
             # Only sleep if a token revocation occurred as a result of role
             # overriding. This will optimize test runtime in the case where
-            # ``[identity] admin_role`` == ``[patrole] rbac_test_role``.
-            if not role_already_present:
+            # ``[identity] admin_role`` == ``[patrole] rbac_test_roles``.
+            if not roles_already_present:
                 time.sleep(1)
 
             for provider in auth_providers:
@@ -164,41 +165,53 @@
         role_map = {r['name']: r['id'] for r in available_roles}
         LOG.debug('Available roles: %s', list(role_map.keys()))
 
-        admin_role_id = role_map.get(CONF.identity.admin_role)
-        rbac_role_id = role_map.get(CONF.patrole.rbac_test_role)
+        rbac_role_ids = []
+        roles = CONF.patrole.rbac_test_roles
+        # TODO(vegasq) drop once CONF.patrole.rbac_test_role is removed
+        if CONF.patrole.rbac_test_role:
+            if not roles:
+                roles.append(CONF.patrole.rbac_test_role)
 
-        if not all([admin_role_id, rbac_role_id]):
+        for role_name in roles:
+            rbac_role_ids.append(role_map.get(role_name))
+
+        admin_role_id = role_map.get(CONF.identity.admin_role)
+
+        if not all([admin_role_id, all(rbac_role_ids)]):
             missing_roles = []
-            msg = ("Could not find `[patrole] rbac_test_role` or "
+            msg = ("Could not find `[patrole] rbac_test_roles` or "
                    "`[identity] admin_role`, both of which are required for "
                    "RBAC testing.")
             if not admin_role_id:
                 missing_roles.append(CONF.identity.admin_role)
-            if not rbac_role_id:
-                missing_roles.append(CONF.patrole.rbac_test_role)
+            if not all(rbac_role_ids):
+                missing_roles += [role_name for role_name in roles
+                                  if not role_map.get(role_name)]
+
             msg += " Following roles were not found: %s." % (
                 ", ".join(missing_roles))
             msg += " Available roles: %s." % ", ".join(list(role_map.keys()))
             raise rbac_exceptions.RbacResourceSetupFailed(msg)
 
         self.admin_role_id = admin_role_id
-        self.rbac_role_id = rbac_role_id
+        self.rbac_role_ids = rbac_role_ids
 
-    def _create_user_role_on_project(self, role_id):
-        self.admin_roles_client.create_user_role_on_project(
-            self.project_id, self.user_id, role_id)
+    def _create_user_role_on_project(self, role_ids):
+        for role_id in role_ids:
+            self.admin_roles_client.create_user_role_on_project(
+                self.project_id, self.user_id, role_id)
 
-    def _list_and_clear_user_roles_on_project(self, role_id):
+    def _list_and_clear_user_roles_on_project(self, role_ids):
         roles = self.admin_roles_client.list_user_roles_on_project(
             self.project_id, self.user_id)['roles']
-        role_ids = [role['id'] for role in roles]
+        all_role_ids = [role['id'] for role in roles]
 
-        # NOTE(felipemonteiro): We do not use ``role_id in role_ids`` here to
-        # avoid over-permission errors: if the current list of roles on the
+        # NOTE(felipemonteiro): We do not use ``role_id in all_role_ids`` here
+        # to avoid over-permission errors: if the current list of roles on the
         # project includes "admin" and "Member", and we are switching to the
         # "Member" role, then we must delete the "admin" role. Thus, we only
         # return early if the user's roles on the project are an exact match.
-        if [role_id] == role_ids:
+        if set(role_ids) == set(all_role_ids):
             return True
 
         for role in roles:
@@ -279,8 +292,14 @@
 def is_admin():
     """Verifies whether the current test role equals the admin role.
 
-    :returns: True if ``rbac_test_role`` is the admin role.
+    :returns: True if ``rbac_test_roles`` contain the admin role.
     """
+    roles = CONF.patrole.rbac_test_roles
+    # TODO(vegasq) drop once CONF.patrole.rbac_test_role is removed
+    if CONF.patrole.rbac_test_role:
+        roles.append(CONF.patrole.rbac_test_role)
+        roles = list(set(roles))
+
     # TODO(felipemonteiro): Make this more robust via a context is admin
     # lookup.
-    return CONF.patrole.rbac_test_role == CONF.identity.admin_role
+    return CONF.identity.admin_role in roles