Implement basic protection testing jobs

This commit lays down a basic structure for protection tests. These are
useful for testing various secure RBAC personas, but leveraging all the
dynamic credential work in tempest's authentication libraries to
provision clients for testing. We're also adding a non-voting protection
test job so that we can integrate protection testing into the cinder
gate as we work through policy changes.

This commit also adds some basic tests exercising the capabilities
admin-only API. These tests ensure that only operators (e.g.,
system-administrators) or formally known as project-administrators, can
access the capabilities API. Assertions and functionality in these tests
may expand in the future to accomodate system-scope when cinder can
properly consume system-scoped tokens from keystone.

For now, the tests assume project-administrators are deployment
operators, which is the legacy way of denoting "admin-ness" in OpenStack
deployments.

Depends-On: https://review.opendev.org/c/openstack/tempest/+/778753

Change-Id: I6d4ae6d516f4c2dda4dcb6b974857b34f2ef2254
diff --git a/.zuul.yaml b/.zuul.yaml
index 87f89f0..4c33fed 100644
--- a/.zuul.yaml
+++ b/.zuul.yaml
@@ -14,6 +14,9 @@
         - cinder-tempest-plugin-basic-victoria
         - cinder-tempest-plugin-basic-ussuri
         - cinder-tempest-plugin-basic-train
+        # Set this job to voting once we have some actual tests to run
+        - cinder-tempest-plugin-protection-functional:
+            voting: false
     gate:
       jobs:
         - cinder-tempest-plugin-lvm-lio-barbican
@@ -26,6 +29,26 @@
         - cinder-tempest-plugin-cbak-ceph-train
 
 - job:
+    name: cinder-tempest-plugin-protection-functional
+    parent: devstack-tempest
+    required-projects:
+      - opendev.org/openstack/cinder-tempest-plugin
+      - opendev.org/openstack/cinder
+    vars:
+      tox_envlist: all
+      tempest_test_regex: 'cinder_tempest_plugin.rbac'
+      devstack_local_conf:
+        test-config:
+          $CINDER_CONF:
+            oslo_policy:
+              enforce_new_defaults: True
+          $TEMPEST_CONFIG:
+            enforce_scope:
+              cinder: True
+      tempest_plugins:
+        - cinder-tempest-plugin
+
+- job:
     name: cinder-tempest-plugin-lvm-barbican-base-abstract
     description: |
       This is a base job for lvm with lio & tgt targets
diff --git a/cinder_tempest_plugin/rbac/__init__.py b/cinder_tempest_plugin/rbac/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/cinder_tempest_plugin/rbac/__init__.py
diff --git a/cinder_tempest_plugin/rbac/v3/__init__.py b/cinder_tempest_plugin/rbac/v3/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/cinder_tempest_plugin/rbac/v3/__init__.py
diff --git a/cinder_tempest_plugin/rbac/v3/base.py b/cinder_tempest_plugin/rbac/v3/base.py
new file mode 100644
index 0000000..d1a11e5
--- /dev/null
+++ b/cinder_tempest_plugin/rbac/v3/base.py
@@ -0,0 +1,42 @@
+#    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 import config
+
+CONF = config.CONF
+
+
+class VolumeV3RbacBaseTests(object):
+
+    identity_version = 'v3'
+
+    @classmethod
+    def skip_checks(cls):
+        super(VolumeV3RbacBaseTests, cls).skip_checks()
+        if not CONF.enforce_scope.cinder:
+            raise cls.skipException(
+                "Tempest is not configured to enforce_scope for cinder, "
+                "skipping RBAC tests. To enable these tests set "
+                "`tempest.conf [enforce_scope] cinder=True`."
+            )
+
+    def do_request(self, method, expected_status=200, client=None, **payload):
+        if not client:
+            client = self.client
+        if isinstance(expected_status, type(Exception)):
+            self.assertRaises(expected_status,
+                              getattr(client, method),
+                              **payload)
+        else:
+            response = getattr(client, method)(**payload)
+            self.assertEqual(response.response.status, expected_status)
+            return response
diff --git a/cinder_tempest_plugin/rbac/v3/test_capabilities.py b/cinder_tempest_plugin/rbac/v3/test_capabilities.py
new file mode 100644
index 0000000..1fa542d
--- /dev/null
+++ b/cinder_tempest_plugin/rbac/v3/test_capabilities.py
@@ -0,0 +1,80 @@
+#    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.
+
+import abc
+
+from tempest.lib import exceptions
+
+from cinder_tempest_plugin.api.volume import base
+from cinder_tempest_plugin.rbac.v3 import base as rbac_base
+
+
+class VolumeV3RbacCapabilityTests(rbac_base.VolumeV3RbacBaseTests,
+                                  metaclass=abc.ABCMeta):
+
+    @classmethod
+    def setup_clients(cls):
+        super().setup_clients()
+        cls.persona = getattr(cls, 'os_%s' % cls.credentials[0])
+        cls.client = cls.persona.volume_capabilities_client_latest
+        # NOTE(lbragstad): This admin_client will be more useful later when
+        # cinder supports system-scope and we need it for administrative
+        # operations. For now, keep os_project_admin as the admin client until
+        # we have system-scope.
+        admin_client = cls.os_project_admin
+        cls.admin_capabilities_client = (
+            admin_client.volume_capabilities_client_latest)
+        cls.admin_stats_client = (
+            admin_client.volume_scheduler_stats_client_latest)
+
+    @classmethod
+    def setup_credentials(cls):
+        super().setup_credentials()
+        cls.os_primary = getattr(cls, 'os_%s' % cls.credentials[0])
+
+    @abc.abstractmethod
+    def test_get_capabilities(self):
+        """Test volume_extension:capabilities policy.
+
+        This test must check:
+          * whether the persona can fetch capabilities for a host.
+
+        """
+        pass
+
+
+class ProjectAdminTests(VolumeV3RbacCapabilityTests, base.BaseVolumeTest):
+
+    credentials = ['project_admin', 'system_admin']
+
+    def test_get_capabilities(self):
+        pools = self.admin_stats_client.list_pools()['pools']
+        host_name = pools[0]['name']
+        self.do_request('show_backend_capabilities', expected_status=200,
+                        host=host_name)
+
+
+class ProjectMemberTests(ProjectAdminTests, base.BaseVolumeTest):
+
+    credentials = ['project_member', 'project_admin', 'system_admin']
+
+    def test_get_capabilities(self):
+        pools = self.admin_stats_client.list_pools()['pools']
+        host_name = pools[0]['name']
+        self.do_request('show_backend_capabilities',
+                        expected_status=exceptions.Forbidden,
+                        host=host_name)
+
+
+class ProjectReaderTests(ProjectMemberTests, base.BaseVolumeTest):
+
+    credentials = ['project_reader', 'project_admin', 'system_admin']