Expand Designate RBAC testing - zones

This patch adds RBAC testing for allowed and disallowed credentials.
This is one of a series of patches adding testing. This patch covers the
zones API.

Change-Id: I2312e0e4293b60d9644f8c0d3a41e0b5f330c20d
diff --git a/designate_tempest_plugin/tests/api/v2/test_blacklists.py b/designate_tempest_plugin/tests/api/v2/test_blacklists.py
index 6dea9ad..cd859af 100644
--- a/designate_tempest_plugin/tests/api/v2/test_blacklists.py
+++ b/designate_tempest_plugin/tests/api/v2/test_blacklists.py
@@ -63,7 +63,7 @@
             expected_allowed = ['os_system_admin']
 
         self.check_CUD_RBAC_enforcement('BlacklistsClient', 'create_blacklist',
-                                        expected_allowed)
+                                        expected_allowed, False)
 
     @decorators.idempotent_id('ea608152-da3c-11eb-b8b8-74e5f9e2a801')
     @decorators.skip_because(bug="1934252")
@@ -106,7 +106,7 @@
             expected_allowed = ['os_system_admin', 'os_system_reader']
 
         self.check_list_show_RBAC_enforcement(
-            'BlacklistsClient', 'show_blacklist', expected_allowed,
+            'BlacklistsClient', 'show_blacklist', expected_allowed, False,
             blacklist['id'])
 
     @decorators.idempotent_id('dcea40d9-8d36-43cb-8440-4a842faaef0d')
@@ -126,8 +126,9 @@
         if CONF.dns_feature_enabled.enforce_new_defaults:
             expected_allowed = ['os_system_admin']
 
-        self.check_CUD_RBAC_enforcement('BlacklistsClient', 'delete_blacklist',
-                                        expected_allowed, blacklist['id'])
+        self.check_CUD_RBAC_enforcement(
+            'BlacklistsClient', 'delete_blacklist', expected_allowed, False,
+            blacklist['id'])
 
     @decorators.idempotent_id('3a2a1e6c-8176-428c-b5dd-d85217c0209d')
     def test_list_blacklists(self):
@@ -173,7 +174,7 @@
             expected_allowed = ['os_system_admin']
 
         self.check_CUD_RBAC_enforcement(
-            'BlacklistsClient', 'update_blacklist', expected_allowed,
+            'BlacklistsClient', 'update_blacklist', expected_allowed, False,
             uuid=blacklist['id'], pattern=pattern, description=description)
 
 
diff --git a/designate_tempest_plugin/tests/api/v2/test_zones.py b/designate_tempest_plugin/tests/api/v2/test_zones.py
index aac3bda..3555c51 100644
--- a/designate_tempest_plugin/tests/api/v2/test_zones.py
+++ b/designate_tempest_plugin/tests/api/v2/test_zones.py
@@ -57,7 +57,6 @@
 
 
 class ZonesTest(BaseZonesTest):
-    credentials = ["admin", "system_admin", "primary"]
 
     @classmethod
     def setup_credentials(cls):
@@ -105,6 +104,24 @@
         self.assertEqual(const.CREATE, zone['action'])
         self.assertEqual(const.PENDING, zone['status'])
 
+        # Test with no extra header overrides (sudo-project-id)
+        expected_allowed = ['os_admin', 'os_primary', 'os_alt']
+        if CONF.dns_feature_enabled.enforce_new_defaults:
+            expected_allowed.append('os_system_admin')
+            expected_allowed.append('os_project_member')
+
+        self.check_CUD_RBAC_enforcement('ZonesClient', 'create_zone',
+                                        expected_allowed, False)
+
+        # Test with x-auth-sudo-project-id header
+        expected_allowed = ['os_admin']
+        if CONF.dns_feature_enabled.enforce_new_defaults:
+            expected_allowed.append('os_system_admin')
+
+        self.check_CUD_RBAC_enforcement(
+            'ZonesClient', 'create_zone', expected_allowed, False,
+            project_id=self.client.project_id)
+
     @decorators.idempotent_id('ec150c22-f52e-11eb-b09b-74e5f9e2a801')
     def test_create_zone_validate_recordsets_created(self):
         # Create a PRIMARY zone and wait till it's Active
@@ -144,6 +161,27 @@
         LOG.info('Ensure the fetched response matches the created zone')
         self.assertExpected(zone, body, self.excluded_keys)
 
+        # TODO(johnsom) Test reader roles once this bug is fixed.
+        #               https://bugs.launchpad.net/tempest/+bug/1964509
+        # Test with no extra header overrides (all_projects, sudo-project-id)
+        expected_allowed = ['os_primary']
+
+        self.check_list_show_RBAC_enforcement(
+            'ZonesClient', 'show_zone', expected_allowed, True, zone['id'])
+
+        # Test with x-auth-all-projects and x-auth-sudo-project-id header
+        if CONF.dns_feature_enabled.enforce_new_defaults:
+            expected_allowed = ['os_system_admin']
+        else:
+            expected_allowed = ['os_admin']
+
+        self.check_list_show_RBAC_enforcement(
+            'ZonesClient', 'show_zone', expected_allowed, False, zone['id'],
+            headers=self.all_projects_header)
+        self.check_list_show_RBAC_enforcement(
+            'ZonesClient', 'show_zone', expected_allowed, False, zone['id'],
+            headers={'x-auth-sudo-project-id': self.client.project_id})
+
     @decorators.idempotent_id('49268b24-92de-11eb-9d02-74e5f9e2a801')
     def test_show_not_existing_zone(self):
         LOG.info('Fetch non existing zone')
@@ -166,6 +204,26 @@
         self.addCleanup(self.wait_zone_delete, self.client, zone['id'],
                         ignore_errors=lib_exc.NotFound)
 
+        # Test RBAC
+        expected_allowed = ['os_admin', 'os_primary']
+        if CONF.dns_feature_enabled.enforce_new_defaults:
+            expected_allowed.append('os_system_admin')
+
+        self.check_CUD_RBAC_enforcement('ZonesClient', 'delete_zone',
+                                        expected_allowed, True, zone['id'])
+
+        # Test RBAC with x-auth-all-projects and x-auth-sudo-project-id header
+        expected_allowed = ['os_admin', 'os_primary']
+        if CONF.dns_feature_enabled.enforce_new_defaults:
+            expected_allowed.append('os_system_admin')
+
+        self.check_CUD_RBAC_enforcement('ZonesClient', 'delete_zone',
+                                        expected_allowed, False, zone['id'],
+                                        headers=self.all_projects_header)
+        self.check_CUD_RBAC_enforcement(
+            'ZonesClient', 'delete_zone', expected_allowed, False, zone['id'],
+            headers={'x-auth-sudo-project-id': self.client.project_id})
+
         LOG.info('Delete the zone')
         body = self.client.delete_zone(zone['id'])[1]
 
@@ -194,6 +252,39 @@
         #              present in the response.
         self.assertGreater(len(body['zones']), 0)
 
+        # TODO(johnsom) Test reader role once this bug is fixed:
+        #               https://bugs.launchpad.net/tempest/+bug/1964509
+        # Test RBAC - Users that are allowed to call list, but should get
+        #             zero zones.
+        if CONF.dns_feature_enabled.enforce_new_defaults:
+            expected_allowed = ['os_system_admin', 'os_system_reader',
+                                'os_admin', 'os_project_member',
+                                'os_project_reader']
+        else:
+            expected_allowed = ['os_alt']
+
+        self.check_list_RBAC_enforcement_count(
+            'ZonesClient', 'list_zones', expected_allowed, 0)
+
+        # Test that users who should see the zone, can see it.
+        expected_allowed = ['os_primary']
+
+        self.check_list_IDs_RBAC_enforcement(
+            'ZonesClient', 'list_zones', expected_allowed, [zone['id']])
+
+        # Test RBAC with x-auth-all-projects and x-auth-sudo-project-id header
+        if CONF.dns_feature_enabled.enforce_new_defaults:
+            expected_allowed = ['os_system_admin']
+        else:
+            expected_allowed = ['os_admin']
+
+        self.check_list_IDs_RBAC_enforcement(
+            'ZonesClient', 'list_zones', expected_allowed, [zone['id']],
+            headers=self.all_projects_header)
+        self.check_list_IDs_RBAC_enforcement(
+            'ZonesClient', 'list_zones', expected_allowed, [zone['id']],
+            headers={'x-auth-sudo-project-id': self.client.project_id})
+
     @decorators.idempotent_id('123f51cb-19d5-48a9-aacc-476742c02141')
     def test_update_zone(self):
         LOG.info('Create a zone')
@@ -216,6 +307,29 @@
         LOG.info('Ensure we respond with updated values')
         self.assertEqual(description, zone['description'])
 
+        # Test RBAC
+        expected_allowed = ['os_admin', 'os_primary']
+        if CONF.dns_feature_enabled.enforce_new_defaults:
+            expected_allowed.append('os_system_admin')
+
+        self.check_CUD_RBAC_enforcement(
+            'ZonesClient', 'update_zone', expected_allowed, True,
+            zone['id'], description=description)
+
+        # Test RBAC with x-auth-all-projects and x-auth-sudo-project-id header
+        expected_allowed = ['os_admin', 'os_primary']
+        if CONF.dns_feature_enabled.enforce_new_defaults:
+            expected_allowed.append('os_system_admin')
+
+        self.check_CUD_RBAC_enforcement(
+            'ZonesClient', 'update_zone', expected_allowed, False,
+            zone['id'], description=description,
+            headers=self.all_projects_header)
+        self.check_CUD_RBAC_enforcement(
+            'ZonesClient', 'update_zone', expected_allowed, False,
+            zone['id'], description=description,
+            headers={'x-auth-sudo-project-id': self.client.project_id})
+
     @decorators.idempotent_id('3acddc86-62cc-4bfa-8589-b99e5d239bf2')
     @decorators.skip_because(bug="1960487")
     def test_serial_changes_on_update(self):
@@ -302,6 +416,29 @@
             pool_nameservers, zone_nameservers,
             'Failed - Pool and Zone nameservers should be the same')
 
+        # TODO(johnsom) Test reader role once this bug is fixed:
+        #               https://bugs.launchpad.net/tempest/+bug/1964509
+        # Test RBAC
+        expected_allowed = ['os_primary']
+
+        self.check_list_show_RBAC_enforcement(
+            'ZonesClient', 'show_zone_nameservers', expected_allowed,
+            True, zone['id'])
+
+        # Test with x-auth-all-projects and x-auth-sudo-project-id header
+        if CONF.dns_feature_enabled.enforce_new_defaults:
+            expected_allowed = ['os_system_admin']
+        else:
+            expected_allowed = ['os_admin']
+
+        self.check_list_show_RBAC_enforcement(
+            'ZonesClient', 'show_zone_nameservers', expected_allowed,
+            False, zone['id'], headers=self.all_projects_header)
+        self.check_list_show_RBAC_enforcement(
+            'ZonesClient', 'show_zone_nameservers', expected_allowed,
+            False, zone['id'],
+            headers={'x-auth-sudo-project-id': self.client.project_id})
+
 
 class ZonesAdminTest(BaseZonesTest):
     credentials = ["primary", "admin", "system_admin", "alt"]
diff --git a/designate_tempest_plugin/tests/base.py b/designate_tempest_plugin/tests/base.py
index 9035c83..e581e0a 100644
--- a/designate_tempest_plugin/tests/base.py
+++ b/designate_tempest_plugin/tests/base.py
@@ -67,9 +67,10 @@
     # have cls.os_alt, and admin will have cls.os_admin.
     # NOTE(johnsom) We will allocate most credentials here so that each test
     # can test for allowed and disallowed RBAC policies.
-    credentials = ['admin', 'primary']
+    credentials = ['admin', 'primary', 'alt']
     if CONF.dns_feature_enabled.enforce_new_defaults:
-        credentials.extend(['system_admin', 'system_reader', 'project_reader'])
+        credentials.extend(['system_admin', 'system_reader',
+                            'project_member', 'project_reader'])
 
     # A tuple of credentials that will be allocated by tempest using the
     # 'credentials' list above. These are used to build RBAC test lists.
diff --git a/designate_tempest_plugin/tests/rbac_utils.py b/designate_tempest_plugin/tests/rbac_utils.py
index e02fdf1..a0148bb 100644
--- a/designate_tempest_plugin/tests/rbac_utils.py
+++ b/designate_tempest_plugin/tests/rbac_utils.py
@@ -75,9 +75,13 @@
                 self.fail('Method {}.{} failed to allow access via RBAC using '
                           'credential {}. Error: {}'.format(
                               client_str, method_str, cred, str(e)))
+            except exceptions.NotFound as e:
+                self.fail('Method {}.{} failed to allow access via RBAC using '
+                          'credential {}. Error: {}'.format(
+                              client_str, method_str, cred, str(e)))
 
     def _check_disallowed(self, client_str, method_str, allowed_list,
-                          *args, **kwargs):
+                          expect_404, *args, **kwargs):
         """Test an API call disallowed RBAC enforcement.
 
         :param client_str: The service client to use for the test, without the
@@ -86,6 +90,7 @@
                            Example: 'list_zones'
         :param allowed_list: The list of credentials expected to be
                              allowed.  Example: ['primary'].
+        :param expect_404: When True, 404 responses are considered ok.
         :param args: Any positional parameters needed by the method.
         :param kwargs: Any named parameters needed by the method.
         :raises AssertionError: Raised if the RBAC tests fail.
@@ -118,11 +123,18 @@
                 method(*args, **kwargs)
             except exceptions.Forbidden:
                 continue
+            except exceptions.NotFound:
+                # Some APIs hide that the resource exists by returning 404
+                # on permission denied.
+                if expect_404:
+                    continue
+                raise
             self.fail('Method {}.{} failed to deny access via RBAC using '
                       'credential {}.'.format(client_str, method_str, cred))
 
     def check_list_show_RBAC_enforcement(self, client_str, method_str,
-                                   expected_allowed, *args, **kwargs):
+                                         expected_allowed, expect_404,
+                                         *args, **kwargs):
         """Test list or show API call RBAC enforcement.
 
         :param client_str: The service client to use for the test, without the
@@ -131,6 +143,7 @@
                            Example: 'list_zones'
         :param expected_allowed: The list of credentials expected to be
                                  allowed.  Example: ['primary'].
+        :param expect_404: When True, 404 responses are considered ok.
         :param args: Any positional parameters needed by the method.
         :param kwargs: Any named parameters needed by the method.
         :raises AssertionError: Raised if the RBAC tests fail.
@@ -145,7 +158,7 @@
 
         # #### Test that disallowed credentials cannot access the API.
         self._check_disallowed(client_str, method_str, allowed_list,
-                               *args, **kwargs)
+                               expect_404, *args, **kwargs)
 
         # #### Test that allowed credentials can access the API.
         self._check_allowed(client_str, method_str, allowed_list,