Merge "Add tests to cover policy_bandwidth_limit_rule"
diff --git a/.zuul.yaml b/.zuul.yaml
index 2e29ccf..fb110f0 100644
--- a/.zuul.yaml
+++ b/.zuul.yaml
@@ -2,8 +2,8 @@
name: patrole-base
parent: devstack-tempest
description: |
- Patrole base job for admin and member roles. This job executes RBAC tests
- for all the "core" services that Tempest covers, excluding Swift.
+ Patrole base job for admin and member roles. This job executes RBAC tests
+ for all the "core" services that Tempest covers, excluding Swift.
required-projects:
- name: openstack/tempest
- name: openstack/patrole
@@ -25,6 +25,7 @@
devstack_services:
tempest: true
neutron: true
+ neutron-trunk: true
tempest_concurrency: 2
tempest_test_regex: (?!.*\[.*\bslow\b.*\])(^patrole_tempest_plugin\.tests\.api)
tox_envlist: all-plugin
@@ -86,6 +87,11 @@
RBAC_TEST_ROLE: member
- job:
+ name: patrole-member-rocky
+ parent: patrole-member
+ override-checkout: stable/rocky
+
+- job:
name: patrole-member-queens
parent: patrole-member
override-checkout: stable/queens
@@ -133,16 +139,15 @@
name: patrole-plugin-base
parent: patrole-base
description: |
- Patrole plugin job for admin and member roles which
- runs RBAC tests for neutron-tempest-plugin APIs (if the plugin is installed).
+ Patrole plugin job for admin and member roles which
+ runs RBAC tests for neutron-tempest-plugin APIs (if the plugin is installed).
required-projects:
- name: openstack/tempest
- name: openstack/patrole
- name: openstack/neutron-tempest-plugin
vars:
devstack_localrc:
- TEMPEST_PLUGINS: "'/opt/stack/patrole
- /opt/stack/neutron-tempest-plugin'"
+ TEMPEST_PLUGINS: "'/opt/stack/patrole /opt/stack/neutron-tempest-plugin'"
devstack_plugins:
neutron: git://git.openstack.org/openstack/neutron.git
patrole: git://git.openstack.org/openstack/patrole.git
@@ -172,19 +177,33 @@
tempest_test_regex: (?=.*PluginRbacTest)(^patrole_tempest_plugin\.tests\.api)
- project:
+ templates:
+ - openstack-cover-jobs
+ - openstack-lower-constraints-jobs
+ - openstack-python36-jobs
+ - openstack-python-jobs
+ - openstack-python35-jobs
+ - check-requirements
+ - publish-openstack-docs-pti
+ - release-notes-jobs-python3
check:
jobs:
- patrole-admin
- patrole-member
+ - patrole-member-rocky
- patrole-member-queens
- patrole-member-pike
- patrole-py35-member
- patrole-multinode-admin
- patrole-multinode-member
- - openstack-tox-lower-constraints
- patrole-plugin-admin
- patrole-plugin-member
gate:
jobs:
- patrole-admin
- patrole-member
+ periodic-stable:
+ jobs:
+ - patrole-member-rocky
+ - patrole-member-queens
+ - patrole-member-pike
diff --git a/devstack/plugin.sh b/devstack/plugin.sh
index bd0068b..a6259f4 100644
--- a/devstack/plugin.sh
+++ b/devstack/plugin.sh
@@ -26,15 +26,31 @@
iniset $TEMPEST_CONFIG policy-feature-enabled volume_extension_volume_actions_attach_policy False
iniset $TEMPEST_CONFIG policy-feature-enabled volume_extension_volume_actions_reserve_policy False
iniset $TEMPEST_CONFIG policy-feature-enabled volume_extension_volume_actions_unreserve_policy False
+
+ # These policies were removed in Stein but are available in Pike.
+ iniset $TEMPEST_CONFIG policy-feature-enabled removed_nova_policies_stein False
+
+ # TODO(cl566n): Policies used by Patrole testing. Remove these once stable/pike becomes EOL.
+ iniset $TEMPEST_CONFIG policy-feature-enabled added_cinder_policies_stein False
fi
if [[ ${DEVSTACK_SERIES} == 'queens' ]]; then
if [[ "$RBAC_TEST_ROLE" == "member" ]]; then
RBAC_TEST_ROLE="Member"
fi
+
+ # These policies were removed in Stein but are available in Queens.
+ iniset $TEMPEST_CONFIG policy-feature-enabled removed_nova_policies_stein False
+
+ # TODO(cl566n): Policies used by Patrole testing. Remove these once stable/queens becomes EOL.
+ iniset $TEMPEST_CONFIG policy-feature-enabled added_cinder_policies_stein False
fi
- iniset $TEMPEST_CONFIG patrole enable_rbac True
+ if [[ ${DEVSTACK_SERIES} == 'rocky' ]]; then
+ # TODO(cl566n): Policies used by Patrole testing. Remove these once stable/rocky becomes EOL.
+ iniset $TEMPEST_CONFIG policy-feature-enabled added_cinder_policies_stein False
+ fi
+
iniset $TEMPEST_CONFIG patrole rbac_test_role $RBAC_TEST_ROLE
}
diff --git a/doc/source/framework/overview.rst b/doc/source/framework/overview.rst
index 4902f7b..113d461 100644
--- a/doc/source/framework/overview.rst
+++ b/doc/source/framework/overview.rst
@@ -1,6 +1,8 @@
RBAC Testing Validation
=======================
+.. _framework-overview:
+
--------
Overview
--------
diff --git a/doc/source/framework/policy_authority.rst b/doc/source/framework/policy_authority.rst
index 822c7b6..37b698c 100644
--- a/doc/source/framework/policy_authority.rst
+++ b/doc/source/framework/policy_authority.rst
@@ -60,3 +60,4 @@
.. automodule:: patrole_tempest_plugin.policy_authority
:members:
:undoc-members:
+ :special-members:
diff --git a/doc/source/framework/rbac_authority.rst b/doc/source/framework/rbac_authority.rst
index 84c372b..40f2a8d 100644
--- a/doc/source/framework/rbac_authority.rst
+++ b/doc/source/framework/rbac_authority.rst
@@ -35,3 +35,4 @@
.. automodule:: patrole_tempest_plugin.rbac_authority
:members:
:undoc-members:
+ :special-members:
diff --git a/doc/source/framework/rbac_utils.rst b/doc/source/framework/rbac_utils.rst
index 7143928..b13a4a3 100644
--- a/doc/source/framework/rbac_utils.rst
+++ b/doc/source/framework/rbac_utils.rst
@@ -23,156 +23,14 @@
and test execution, respectively. This is especially true when considering
custom policy rule definitions, which can be arbitrarily complex.
-.. _role-overriding:
-
-Role Overriding
-^^^^^^^^^^^^^^^
-
-Role overriding is the way Patrole is able to create resources and delete
-resources -- including those that require admin credentials -- while still
-being able to exercise the same set of Tempest credentials to perform the API
-action that authorizes the policy under test, by manipulating the role of
-the Tempest credentials.
-
-Patrole implicitly splits up each test into 3 stages: set up, test execution,
-and teardown.
-
-The role workflow is as follows:
-
-#. Setup: Admin role is used automatically. The primary credentials are
- overridden with the admin role.
-#. Test execution: ``[patrole] rbac_test_role`` is used manually via the
- call to ``with rbac_utils.override_role(self)``. Everything that
- is executed within this contextmanager uses the primary
- credentials overridden with the ``[patrole] rbac_test_role``.
-#. Teardown: Admin role is used automatically. The primary credentials have
- been overridden with the admin role.
-
-.. _Tempest credentials: https://docs.openstack.org/tempest/latest/library/credential_providers.html
-.. _dynamic credentials: https://docs.openstack.org/tempest/latest/configuration.html#dynamic-credentials
-
-Test Setup
-----------
-
-Automatic role override in background.
-
-Resources can be set up inside the ``resource_setup`` class method that Tempest
-provides. These resources are typically reserved for "expensive" resources
-in terms of memory or storage requirements, like volumes and VMs. These
-resources are **always** created via the admin role; Patrole automatically
-handles this.
-
-Like Tempest, however, Patrole must also create resources inside tests
-themselves. At the beginning of each test, the primary credentials have already
-been overridden with the admin role. One can create whatever test-level
-resources one needs, without having to worry about permissions.
-
-Test Execution
---------------
-
-Manual role override required.
-
-"Test execution" here means calling the API endpoint that enforces the policy
-action expected by the ``rbac_rule_validation`` decorator. Test execution
-should be performed *only after* calling
-``with rbac_utils.override_role(self)``.
-
-Immediately after that call, the API endpoint that enforces the policy should
-be called.
-
-Examples
-^^^^^^^^
-
-Always use the contextmanager before calling the API that enforces the
-expected policy action.
-
-Example::
-
- @rbac_rule_validation.action(
- service="nova",
- rule="os_compute_api:os-aggregates:show")
- def test_show_aggregate_rbac(self):
- # Do test setup before the ``override_role`` call.
- aggregate_id = self._create_aggregate()
- # Call the ``override_role`` method so that the primary credentials
- # have the test role needed for test execution.
- with self.rbac_utils.override_role(self):
- self.aggregates_client.show_aggregate(aggregate_id)
-
-When using a waiter, do the wait outside the contextmanager. "Waiting" always
-entails executing a ``GET`` request to the server, until the state of the
-returned resource matches a desired state. These ``GET`` requests enforce
-a different policy than the one expected. This is undesirable because
-Patrole should only test policies in isolation from one another.
-
-Otherwise, the test result will be tainted, because instead of only the
-expected policy getting enforced with the ``os_primary`` role, at least
-two policies get enforced.
-
-Example using waiter::
-
- @rbac_rule_validation.action(
- service="nova",
- rule="os_compute_api:os-admin-password")
- def test_change_server_password(self):
- original_password = self.servers_client.show_password(
- self.server['id'])
- self.addCleanup(self.servers_client.change_password, self.server['id'],
- adminPass=original_password)
-
- with self.rbac_utils.override_role(self):
- self.servers_client.change_password(
- self.server['id'], adminPass=data_utils.rand_password())
- # Call the waiter outside the ``override_role`` contextmanager, so that
- # it is executed with admin role.
- waiters.wait_for_server_status(
- self.servers_client, self.server['id'], 'ACTIVE')
-
-Below is an example of a method that enforces multiple policies getting
-called inside the contextmanager. The ``_complex_setup_method`` below
-performs the correct API that enforces the expected policy -- in this
-case ``self.resources_client.create_resource`` -- but then proceeds to
-use a waiter.
-
-Incorrect::
-
- def _complex_setup_method(self):
- resource = self.resources_client.create_resource(
- **kwargs)['resource']
- self.addCleanup(test_utils.call_and_ignore_notfound_exc,
- self._delete_resource, resource)
- waiters.wait_for_resource_status(
- self.resources_client, resource['id'], 'available')
- return resource
-
- @rbac_rule_validation.action(
- service="example-service",
- rule="example-rule")
- def test_change_server_password(self):
- # Never call a helper function inside the contextmanager that calls a
- # bunch of APIs. Only call the API that enforces the policy action
- # contained in the decorator above.
- with self.rbac_utils.override_role(self):
- self._complex_setup_method()
-
-To fix this test, see the "Example using waiter" section above. It is
-recommended to re-implement the logic in a helper method inside a test such
-that only the relevant API is called inside the contextmanager, with
-everything extraneous outside.
-
-Test Cleanup
-------------
-
-Automatic role override in background.
-
-After the test -- no matter whether it ended successfully or in failure --
-the credentials are overridden with the admin role by the Patrole framework,
-*before* ``tearDown`` or ``tearDownClass`` are called. This means that
-resources are always cleaned up using the admin role.
-
Implementation
--------------
.. automodule:: patrole_tempest_plugin.rbac_utils
:members:
:private-members:
+ :special-members:
+
+.. _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 186dfe2..6cd1534 100644
--- a/doc/source/framework/rbac_validation.rst
+++ b/doc/source/framework/rbac_validation.rst
@@ -17,3 +17,4 @@
.. automodule:: patrole_tempest_plugin.rbac_rule_validation
:members:
:private-members:
+ :special-members:
diff --git a/doc/source/framework/requirements_authority.rst b/doc/source/framework/requirements_authority.rst
index 6c4fcc0..628f0c0 100644
--- a/doc/source/framework/requirements_authority.rst
+++ b/doc/source/framework/requirements_authority.rst
@@ -103,3 +103,4 @@
.. automodule:: patrole_tempest_plugin.requirements_authority
:members:
:undoc-members:
+ :special-members:
diff --git a/doc/source/index.rst b/doc/source/index.rst
index 2dbf63b..c03aac6 100644
--- a/doc/source/index.rst
+++ b/doc/source/index.rst
@@ -54,6 +54,7 @@
HACKING
REVIEWING
+ test_writing_guide
Framework
---------
diff --git a/doc/source/test_writing_guide.rst b/doc/source/test_writing_guide.rst
new file mode 100644
index 0000000..d25f60a
--- /dev/null
+++ b/doc/source/test_writing_guide.rst
@@ -0,0 +1,166 @@
+Patrole Test Writing Overview
+=============================
+
+Introduction
+------------
+
+Patrole tests are broken up into 3 stages:
+
+#. :ref:`rbac-test-setup`
+#. :ref:`rbac-test-execution`
+#. :ref:`rbac-test-cleanup`
+
+See the :ref:`framework overview documentation <framework-overview>` for a
+high-level explanation of the entire testing work flow and framework
+implementation. The guide that follows is concerned with helping developers
+know how to write Patrole tests.
+
+.. _role-overriding:
+
+Role Overriding
+---------------
+
+Role overriding is the way Patrole is able to create resources and delete
+resources -- including those that require admin credentials -- while still
+being able to exercise the same set of Tempest credentials to perform the API
+action that authorizes the policy under test, by manipulating the role of
+the Tempest credentials.
+
+Patrole implicitly splits up each test into 3 stages: set up, test execution,
+and teardown.
+
+The role workflow is as follows:
+
+#. Setup: Admin role is used automatically. The primary credentials are
+ overridden with the admin role.
+#. Test execution: ``[patrole] rbac_test_role`` is used manually via the
+ call to ``with rbac_utils.override_role(self)``. Everything that
+ is executed within this contextmanager uses the primary
+ credentials overridden with the ``[patrole] rbac_test_role``.
+#. Teardown: Admin role is used automatically. The primary credentials have
+ been overridden with the admin role.
+
+.. _rbac-test-setup:
+
+Test Setup
+----------
+
+Automatic role override in background.
+
+Resources can be set up inside the ``resource_setup`` class method that Tempest
+provides. These resources are typically reserved for "expensive" resources
+in terms of memory or storage requirements, like volumes and VMs. These
+resources are **always** created via the admin role; Patrole automatically
+handles this.
+
+Like Tempest, however, Patrole must also create resources inside tests
+themselves. At the beginning of each test, the primary credentials have already
+been overridden with the admin role. One can create whatever test-level
+resources one needs, without having to worry about permissions.
+
+.. _rbac-test-execution:
+
+Test Execution
+--------------
+
+Manual role override required.
+
+"Test execution" here means calling the API endpoint that enforces the policy
+action expected by the ``rbac_rule_validation`` decorator. Test execution
+should be performed *only after* calling
+``with rbac_utils.override_role(self)``.
+
+Immediately after that call, the API endpoint that enforces the policy should
+be called.
+
+Examples
+^^^^^^^^
+
+Always use the contextmanager before calling the API that enforces the
+expected policy action.
+
+Example::
+
+ @rbac_rule_validation.action(
+ service="nova",
+ rule="os_compute_api:os-aggregates:show")
+ def test_show_aggregate_rbac(self):
+ # Do test setup before the ``override_role`` call.
+ aggregate_id = self._create_aggregate()
+ # Call the ``override_role`` method so that the primary credentials
+ # have the test role needed for test execution.
+ with self.rbac_utils.override_role(self):
+ self.aggregates_client.show_aggregate(aggregate_id)
+
+When using a waiter, do the wait outside the contextmanager. "Waiting" always
+entails executing a ``GET`` request to the server, until the state of the
+returned resource matches a desired state. These ``GET`` requests enforce
+a different policy than the one expected. This is undesirable because
+Patrole should only test policies in isolation from one another.
+
+Otherwise, the test result will be tainted, because instead of only the
+expected policy getting enforced with the ``os_primary`` role, at least
+two policies get enforced.
+
+Example using waiter::
+
+ @rbac_rule_validation.action(
+ service="nova",
+ rule="os_compute_api:os-admin-password")
+ def test_change_server_password(self):
+ original_password = self.servers_client.show_password(
+ self.server['id'])
+ self.addCleanup(self.servers_client.change_password, self.server['id'],
+ adminPass=original_password)
+
+ with self.rbac_utils.override_role(self):
+ self.servers_client.change_password(
+ self.server['id'], adminPass=data_utils.rand_password())
+ # Call the waiter outside the ``override_role`` contextmanager, so that
+ # it is executed with admin role.
+ waiters.wait_for_server_status(
+ self.servers_client, self.server['id'], 'ACTIVE')
+
+Below is an example of a method that enforces multiple policies getting
+called inside the contextmanager. The ``_complex_setup_method`` below
+performs the correct API that enforces the expected policy -- in this
+case ``self.resources_client.create_resource`` -- but then proceeds to
+use a waiter.
+
+Incorrect::
+
+ def _complex_setup_method(self):
+ resource = self.resources_client.create_resource(
+ **kwargs)['resource']
+ self.addCleanup(test_utils.call_and_ignore_notfound_exc,
+ self._delete_resource, resource)
+ waiters.wait_for_resource_status(
+ self.resources_client, resource['id'], 'available')
+ return resource
+
+ @rbac_rule_validation.action(
+ service="example-service",
+ rule="example-rule")
+ def test_change_server_password(self):
+ # Never call a helper function inside the contextmanager that calls a
+ # bunch of APIs. Only call the API that enforces the policy action
+ # contained in the decorator above.
+ with self.rbac_utils.override_role(self):
+ self._complex_setup_method()
+
+To fix this test, see the "Example using waiter" section above. It is
+recommended to re-implement the logic in a helper method inside a test such
+that only the relevant API is called inside the contextmanager, with
+everything extraneous outside.
+
+.. _rbac-test-cleanup:
+
+Test Cleanup
+------------
+
+Automatic role override in background.
+
+After the test -- no matter whether it ended successfully or in failure --
+the credentials are overridden with the admin role by the Patrole framework,
+*before* ``tearDown`` or ``tearDownClass`` are called. This means that
+resources are always cleaned up using the admin role.
diff --git a/patrole_tempest_plugin/config.py b/patrole_tempest_plugin/config.py
index ebc8a1d..56a786b 100644
--- a/patrole_tempest_plugin/config.py
+++ b/patrole_tempest_plugin/config.py
@@ -24,23 +24,14 @@
default='admin',
help="""The current RBAC role against which to run
Patrole tests."""),
- cfg.BoolOpt('enable_rbac',
- default=True,
- deprecated_for_removal=True,
- deprecated_reason="""This is a legacy option that was
-meaningful when Patrole existed downstream as a suite of tests inside Tempest.
-Installing the Patrole plugin necessarily means that RBAC tests should be run.
-This option is paradoxical with the Tempest plugin architecture.
-""",
- deprecated_since='R',
- help="Enables Patrole RBAC tests."),
cfg.ListOpt('custom_policy_files',
default=['/etc/%s/policy.json'],
help="""List of the paths to search for policy files. Each
policy path assumes that the service name is included in the path once. Also
assumes Patrole is on the same host as the policy files. The paths should be
-ordered by precedence, with high-priority paths before low-priority paths. The
-first path that is found to contain the service's policy file will be used.
+ordered by precedence, with high-priority paths before low-priority paths. All
+the paths that are found to contain the service's policy file will be used and
+all policy files will be merged. Allowed ``json`` or ``yaml`` formats.
"""),
cfg.BoolOpt('test_custom_requirements',
default=False,
@@ -160,7 +151,22 @@
default=True,
help="""Is the Cinder policy
"volume_extension:volume_actions:unreserve" available in the cloud? This policy
-was changed in a backwards-incompatible way.""")
+was changed in a backwards-incompatible way."""),
+ # *** Include feature flags for groups of policies below. ***
+ # Best practice is to capture new policies, removed policies, renamed
+ # policies in a group, per release.
+ #
+ # TODO(felipemonteiro): Remove these feature flags once Stein is EOL.
+ cfg.BoolOpt('removed_nova_policies_stein',
+ default=True,
+ help="""Are the Nova API extension policies available in the
+cloud (e.g. os_compute_api:os-extended-availability-zone)? These policies were
+removed in Stein because Nova API extension concept was removed in Pike."""),
+ cfg.BoolOpt('added_cinder_policies_stein',
+ default=True,
+ help="""Are the Cinder API extension policies available in the
+cloud (e.g. [create|update|get|delete]_encryption_policy)? These policies are
+added in Stein.""")
]
diff --git a/patrole_tempest_plugin/policy_authority.py b/patrole_tempest_plugin/policy_authority.py
index 3339a5d..2a49b6c 100644
--- a/patrole_tempest_plugin/policy_authority.py
+++ b/patrole_tempest_plugin/policy_authority.py
@@ -13,8 +13,9 @@
# License for the specific language governing permissions and limitations
# under the License.
+import collections
import copy
-import json
+import glob
import os
from oslo_log import log as logging
@@ -103,17 +104,14 @@
if extra_target_data is None:
extra_target_data = {}
- self.validate_service(service)
+ self.service = self.validate_service(service)
# Prioritize dynamically searching for policy files over relying on
# deprecated service-specific policy file locations.
- self.path = None
if CONF.patrole.custom_policy_files:
self.discover_policy_files()
- self.path = self.policy_files.get(service)
- self.rules = policy.Rules.load(self._get_policy_data(service),
- 'default')
+ self.rules = self.get_rules()
self.project_id = project_id
self.user_id = user_id
self.extra_target_data = extra_target_data
@@ -139,19 +137,22 @@
raise rbac_exceptions.RbacInvalidServiceException(
"%s is NOT a valid service." % service)
+ return service
+
@classmethod
def discover_policy_files(cls):
"""Dynamically discover the policy file for each service in
- ``cls.available_services``. Pick the first candidate path found
+ ``cls.available_services``. Pick all candidate paths found
out of the potential paths in ``[patrole] custom_policy_files``.
"""
if not hasattr(cls, 'policy_files'):
- cls.policy_files = {}
+ cls.policy_files = collections.defaultdict(list)
for service in cls.available_services:
for candidate_path in CONF.patrole.custom_policy_files:
- if os.path.isfile(candidate_path % service):
- cls.policy_files.setdefault(service,
- candidate_path % service)
+ path = candidate_path % service
+ for filename in glob.iglob(path):
+ if os.path.isfile(filename):
+ cls.policy_files[service].append(filename)
def allowed(self, rule_name, role):
"""Checks if a given rule in a policy is allowed with given role.
@@ -168,71 +169,60 @@
is_admin=is_admin_context)
return is_allowed
- def _get_policy_data(self, service):
- file_policy_data = {}
- mgr_policy_data = {}
- policy_data = {}
-
+ def get_rules(self):
+ rules = policy.Rules()
# Check whether policy file exists and attempt to read it.
- if self.path and os.path.isfile(self.path):
+ for path in self.policy_files[self.service]:
try:
- with open(self.path, 'r') as policy_file:
- file_policy_data = policy_file.read()
- file_policy_data = json.loads(file_policy_data)
- except (IOError, ValueError) as e:
- msg = "Failed to read policy file for service. "
- if isinstance(e, IOError):
- msg += "Please check that policy path exists."
- else:
- msg += "JSON may be improperly formatted."
- LOG.debug(msg)
- file_policy_data = {}
+ with open(path, 'r') as fp:
+ for k, v in policy.Rules.load(fp.read()).items():
+ if k not in rules:
+ rules[k] = v
+ # If the policy name and rule are the same, no
+ # ambiguity, so no reason to warn.
+ elif str(v) != str(rules[k]):
+ msg = ("The same policy name: %s was found in "
+ "multiple policies files for service %s. "
+ "This can lead to policy rule ambiguity. "
+ "Using rule: %s; Rule from file: %s")
+ LOG.warning(msg, k, self.service, rules[k], v)
+ except (ValueError, IOError):
+ LOG.warning("Failed to read policy file '%s' for service %s.",
+ path, self.service)
# Check whether policy actions are defined in code. Nova and Keystone,
# for example, define their default policy actions in code.
mgr = stevedore.named.NamedExtensionManager(
'oslo.policy.policies',
- names=[service],
- on_load_failure_callback=None,
+ names=[self.service],
invoke_on_load=True,
warn_on_missing_entrypoint=False)
if mgr:
- policy_generator = {policy.name: policy.obj for policy in mgr}
- if policy_generator and service in policy_generator:
- for rule in policy_generator[service]:
- mgr_policy_data[rule.name] = str(rule.check)
+ policy_generator = {plc.name: plc.obj for plc in mgr}
+ if self.service in policy_generator:
+ for rule in policy_generator[self.service]:
+ if rule.name not in rules:
+ rules[rule.name] = rule.check
+ elif str(rule.check) != str(rules[rule.name]):
+ msg = ("The same policy name: %s was found in the "
+ "policies files and in the code for service "
+ "%s. This can lead to policy rule ambiguity. "
+ "Using rule: %s; Rule from code: %s")
+ LOG.warning(msg, rule.name, self.service,
+ rules[rule.name], rule.check)
- # If data from both file and code exist, combine both together.
- if file_policy_data and mgr_policy_data:
- # Add the policy actions from code first.
- for action, rule in mgr_policy_data.items():
- policy_data[action] = rule
- # Overwrite with any custom policy actions defined in policy.json.
- for action, rule in file_policy_data.items():
- policy_data[action] = rule
- elif file_policy_data:
- policy_data = file_policy_data
- elif mgr_policy_data:
- policy_data = mgr_policy_data
- else:
- error_message = (
- 'Policy file for {0} service was not found among the '
+ if not rules:
+ msg = (
+ 'Policy files for {0} service were not found among the '
'registered in-code policies or in any of the possible policy '
- 'files: {1}.'.format(service,
- [loc % service for loc in
- CONF.patrole.custom_policy_files])
- )
- raise rbac_exceptions.RbacParsingException(error_message)
+ 'files: {1}.'.format(
+ self.service,
+ [loc % self.service
+ for loc in CONF.patrole.custom_policy_files]))
+ raise rbac_exceptions.RbacParsingException(msg)
- try:
- policy_data = json.dumps(policy_data)
- except (TypeError, ValueError):
- error_message = 'Policy file for {0} service is invalid.'.format(
- service)
- raise rbac_exceptions.RbacParsingException(error_message)
-
- return policy_data
+ return rules
def _is_admin_context(self, role):
"""Checks whether a role has admin context.
@@ -296,9 +286,11 @@
def _try_rule(self, apply_rule, target, access_data, o):
if apply_rule not in self.rules:
- message = ("Policy action \"{0}\" not found in policy file: {1} or"
- " among registered policy in code defaults for service."
- ).format(apply_rule, self.path)
+ message = ('Policy action "{0}" not found in policy files: '
+ '{1} or among registered policy in code defaults for '
+ '{2} service.').format(apply_rule,
+ self.policy_files[self.service],
+ self.service)
LOG.debug(message)
raise rbac_exceptions.RbacParsingException(message)
else:
diff --git a/patrole_tempest_plugin/rbac_exceptions.py b/patrole_tempest_plugin/rbac_exceptions.py
index 809a7ed..3958e17 100644
--- a/patrole_tempest_plugin/rbac_exceptions.py
+++ b/patrole_tempest_plugin/rbac_exceptions.py
@@ -16,12 +16,16 @@
from tempest.lib import exceptions
-class RbacConflictingPolicies(exceptions.TempestException):
+class BasePatroleException(exceptions.TempestException):
+ message = "An unknown RBAC exception occurred"
+
+
+class RbacConflictingPolicies(BasePatroleException):
message = ("Conflicting policies preventing this action from being "
"performed.")
-class RbacMalformedResponse(exceptions.TempestException):
+class RbacMalformedResponse(BasePatroleException):
message = ("The response body is missing the expected %(attribute)s due "
"to policy enforcement failure.")
@@ -37,25 +41,25 @@
super(RbacMalformedResponse, self).__init__(**kwargs)
-class RbacResourceSetupFailed(exceptions.TempestException):
+class RbacResourceSetupFailed(BasePatroleException):
message = "RBAC resource setup failed"
-class RbacOverPermissionException(exceptions.TempestException):
+class RbacOverPermissionException(BasePatroleException):
"""Raised when the expected result is failure but the actual result is
pass.
"""
message = "Unauthorized action was allowed to be performed"
-class RbacUnderPermissionException(exceptions.TempestException):
+class RbacUnderPermissionException(BasePatroleException):
"""Raised when the expected result is pass but the actual result is
failure.
"""
message = "Authorized action was not allowed to be performed"
-class RbacExpectedWrongException(exceptions.TempestException):
+class RbacExpectedWrongException(BasePatroleException):
"""Raised when the expected exception does not match the actual exception
raised, when both are instances of Forbidden or NotFound, indicating
the test provides a wrong argument to `expected_error_codes`.
@@ -64,16 +68,30 @@
"instead. Actual exception: %(exception)s")
-class RbacInvalidServiceException(exceptions.TempestException):
+class RbacInvalidServiceException(BasePatroleException):
"""Raised when an invalid service is passed to ``rbac_rule_validation``
decorator.
"""
message = "Attempted to test an invalid service"
-class RbacParsingException(exceptions.TempestException):
+class RbacParsingException(BasePatroleException):
message = "Attempted to test an invalid policy file or action"
-class RbacInvalidErrorCode(exceptions.TempestException):
+class RbacInvalidErrorCode(BasePatroleException):
message = "Unsupported error code passed in test"
+
+
+class RbacOverrideRoleException(BasePatroleException):
+ """Raised when override_role is used incorrectly or fails somehow.
+
+ Used for safeguarding against false positives that might occur when the
+ expected exception isn't raised inside the ``override_role`` context.
+ Specifically, when:
+
+ * ``override_role`` isn't called
+ * an exception is raised before ``override_role`` context
+ * an exception is raised after ``override_role`` context
+ """
+ message = "Override role failure or incorrect usage"
diff --git a/patrole_tempest_plugin/rbac_rule_validation.py b/patrole_tempest_plugin/rbac_rule_validation.py
index e6d1e80..d3b057c 100644
--- a/patrole_tempest_plugin/rbac_rule_validation.py
+++ b/patrole_tempest_plugin/rbac_rule_validation.py
@@ -38,8 +38,11 @@
RBACLOG = logging.getLogger('rbac_reporting')
-def action(service, rule='', rules=None,
- expected_error_code=_DEFAULT_ERROR_CODE, expected_error_codes=None,
+def action(service,
+ rule='',
+ rules=None,
+ expected_error_code=_DEFAULT_ERROR_CODE,
+ expected_error_codes=None,
extra_target_data=None):
"""A decorator for verifying OpenStack policy enforcement.
@@ -72,16 +75,18 @@
As such, negative and positive testing can be applied using this decorator.
:param str service: An OpenStack service. Examples: "nova" or "neutron".
- :param str rule: (DEPRECATED) A policy action defined in a policy.json file
- or in code.
- :param list rules: A list of policy actions defined in a policy.json file
+ :param rule: (DEPRECATED) A policy action defined in a policy.json file
+ or in code. Also accepts a callable that returns a policy action.
+ :type rule: str or callable
+ :param rules: A list of policy actions defined in a policy.json file
or in code. The rules are logical-ANDed together to derive the expected
- result.
+ result. Also accepts list of callables that return a policy action.
.. note::
Patrole currently only supports custom JSON policy files.
+ :type rules: list[str] or list[callable]
:param int expected_error_code: (DEPRECATED) Overrides default value of 403
(Forbidden) with endpoint-specific error code. Currently only supports
403 and 404. Support for 404 is needed because some services, like
@@ -110,7 +115,7 @@
c) if both api_action1 and api_action2 fail, then the expected error
code is the first error seen (404).
- If an error code is missing from the list, it is defaulted to 403.
+ If it is not passed, then it is defaulted to 403.
:param dict extra_target_data: Dictionary, keyed with ``oslo.policy``
generic check names, whose values are string literals that reference
@@ -180,6 +185,7 @@
expected_exception, irregular_msg = _get_exception_type(
exp_error_code)
+ caught_exception = None
test_status = 'Allowed'
try:
@@ -193,13 +199,16 @@
LOG.error(msg)
except (expected_exception,
rbac_exceptions.RbacConflictingPolicies,
- rbac_exceptions.RbacMalformedResponse) as e:
+ rbac_exceptions.RbacMalformedResponse) as actual_exception:
+ caught_exception = actual_exception
test_status = 'Denied'
+
if irregular_msg:
LOG.warning(irregular_msg,
test_func.__name__,
', '.join(rules),
service)
+
if allowed:
msg = ("Role %s was not allowed to perform the following "
"actions: %s. Expected allowed actions: %s. "
@@ -209,8 +218,10 @@
sorted(disallowed_rules)))
LOG.error(msg)
raise rbac_exceptions.RbacUnderPermissionException(
- "%s Exception was: %s" % (msg, e))
+ "%s Exception was: %s" % (msg, actual_exception))
except Exception as actual_exception:
+ caught_exception = actual_exception
+
if _check_for_expected_mismatch_exception(expected_exception,
actual_exception):
LOG.error('Expected and actual exceptions do not match. '
@@ -249,6 +260,14 @@
"Allowed" if allowed else "Denied",
test_status)
+ # Sanity-check that ``override_role`` was called to eliminate
+ # false-positives and bad test flows resulting from exceptions
+ # getting raised too early, too late or not at all, within
+ # the scope of an RBAC test.
+ _validate_override_role_called(
+ test_obj,
+ actual_exception=caught_exception)
+
return wrapper
return decorator
@@ -302,7 +321,11 @@
for i in range(num_rules - num_ecs):
exp_error_codes.append(_DEFAULT_ERROR_CODE)
- return rules, exp_error_codes
+ evaluated_rules = [
+ r() if callable(r) else r for r in rules
+ ]
+
+ return evaluated_rules, exp_error_codes
def _is_authorized(test_obj, service, rule, extra_target_data):
@@ -389,7 +412,7 @@
irregular_msg = ("NotFound exception was caught for test %s. Expected "
"policies which may have caused the error: %s. The "
"service %s throws a 404 instead of a 403, which is "
- "irregular.")
+ "irregular")
return expected_exception, irregular_msg
@@ -431,8 +454,63 @@
def _check_for_expected_mismatch_exception(expected_exception,
actual_exception):
+ """Checks that ``expected_exception`` matches ``actual_exception``.
+
+ Since Patrole must handle 403/404 it is important that the expected and
+ actual error codes match.
+
+ :param excepted_exception: Expected exception for test.
+ :param actual_exception: Actual exception raised by test.
+ :returns: True if match, else False.
+ :rtype: boolean
+ """
permission_exceptions = (lib_exc.Forbidden, lib_exc.NotFound)
if isinstance(actual_exception, permission_exceptions):
if not isinstance(actual_exception, expected_exception.__class__):
return True
return False
+
+
+def _validate_override_role_called(test_obj, actual_exception):
+ """Validates that :func:`rbac_utils.RbacUtils.override_role` is called
+ during each Patrole test.
+
+ Useful for validating that the expected exception isn't raised too early
+ (before ``override_role`` call) or too late (after ``override_call``) or
+ at all (which is a bad test).
+
+ :param test_obj: An instance or subclass of ``tempest.test.BaseTestCase``.
+ :param actual_exception: Actual exception raised by test.
+ :raises RbacOverrideRoleException: If ``override_role`` isn't called, is
+ called too early, or is called too late.
+ """
+ called = test_obj._validate_override_role_called()
+ base_msg = ('This error is unrelated to RBAC and is due to either '
+ 'an API or override role failure. Exception: %s' %
+ actual_exception)
+
+ if not called:
+ if actual_exception is not None:
+ msg = ('Caught exception (%s) but it was raised before the '
+ '`override_role` context. ' % actual_exception.__class__)
+ else:
+ msg = 'Test missing required `override_role` call. '
+ msg += base_msg
+ LOG.error(msg)
+ raise rbac_exceptions.RbacOverrideRoleException(msg)
+ else:
+ exc_caught_in_ctx = test_obj._validate_override_role_caught_exc()
+ # This block is only executed if ``override_role`` is called. If
+ # an exception is raised and the exception wasn't raised in the
+ # ``override_role`` context and if the exception isn't a valid
+ # exception type (instance of ``BasePatroleException``), then this is
+ # a legitimate error.
+ if (not exc_caught_in_ctx and
+ actual_exception is not None and
+ not isinstance(actual_exception,
+ rbac_exceptions.BasePatroleException)):
+ msg = ('Caught exception (%s) but it was raised after the '
+ '`override_role` context. ' % actual_exception.__class__)
+ msg += base_msg
+ LOG.error(msg)
+ raise rbac_exceptions.RbacOverrideRoleException(msg)
diff --git a/patrole_tempest_plugin/rbac_utils.py b/patrole_tempest_plugin/rbac_utils.py
index 9a9f864..366e033 100644
--- a/patrole_tempest_plugin/rbac_utils.py
+++ b/patrole_tempest_plugin/rbac_utils.py
@@ -14,10 +14,10 @@
# under the License.
from contextlib import contextmanager
+import sys
import time
from oslo_log import log as logging
-from oslo_log import versionutils
from oslo_utils import excutils
from tempest import clients
@@ -95,11 +95,17 @@
# if the API call above threw an exception, any code below this
# point in the test is not executed.
"""
+ test_obj._set_override_role_called()
self._override_role(test_obj, True)
try:
# Execute the test.
yield
finally:
+ # Check whether an exception was raised. If so, remember that
+ # for future validation.
+ exc = sys.exc_info()[0]
+ if exc is not None:
+ test_obj._set_override_role_caught_exc()
# This code block is always executed, no matter the result of the
# test. Automatically switch back to the admin role for test clean
# up.
@@ -222,6 +228,11 @@
cls.setup_rbac_utils()
"""
+ # Shows if override_role was called.
+ __override_role_called = False
+ # Shows if exception raised during override_role.
+ __override_role_caught_exc = False
+
@classmethod
def get_auth_providers(cls):
"""Returns list of auth_providers used within test.
@@ -232,21 +243,36 @@
return [cls.os_primary.auth_provider]
@classmethod
- def skip_rbac_checks(cls):
- if not CONF.patrole.enable_rbac:
- deprecation_msg = ("The `[patrole].enable_rbac` option is "
- "deprecated and will be removed in the S "
- "release. Patrole tests will always be enabled "
- "following installation of the Patrole Tempest "
- "plugin. Use a regex to skip tests.")
- versionutils.report_deprecated_feature(LOG, deprecation_msg)
- raise cls.skipException(
- 'Patrole testing not enabled so skipping %s.' % cls.__name__)
-
- @classmethod
def setup_rbac_utils(cls):
cls.rbac_utils = RbacUtils(cls)
+ def _set_override_role_called(self):
+ """Helper for tracking whether ``override_role`` was called."""
+ self.__override_role_called = True
+
+ def _set_override_role_caught_exc(self):
+ """Helper for tracking whether exception was thrown inside
+ ``override_role``.
+ """
+ self.__override_role_caught_exc = True
+
+ def _validate_override_role_called(self):
+ """Idempotently validate that ``override_role`` is called and reset
+ its value to False for sequential tests.
+ """
+ was_called = self.__override_role_called
+ self.__override_role_called = False
+ return was_called
+
+ def _validate_override_role_caught_exc(self):
+ """Idempotently validate that exception was caught inside
+ ``override_role``, so that, by process of elimination, it can be
+ determined whether one was thrown outside (which is invalid).
+ """
+ caught_exception = self.__override_role_caught_exc
+ self.__override_role_caught_exc = False
+ return caught_exception
+
def is_admin():
"""Verifies whether the current test role equals the admin role.
diff --git a/patrole_tempest_plugin/tests/api/README.rst b/patrole_tempest_plugin/tests/api/README.rst
new file mode 120000
index 0000000..e2853ec
--- /dev/null
+++ b/patrole_tempest_plugin/tests/api/README.rst
@@ -0,0 +1 @@
+../../../doc/source/field_guide/rbac.rst
\ No newline at end of file
diff --git a/patrole_tempest_plugin/tests/api/compute/rbac_base.py b/patrole_tempest_plugin/tests/api/compute/rbac_base.py
index 18d2f48..ab4551e 100644
--- a/patrole_tempest_plugin/tests/api/compute/rbac_base.py
+++ b/patrole_tempest_plugin/tests/api/compute/rbac_base.py
@@ -12,24 +12,16 @@
# under the License.
from tempest.api.compute import base as compute_base
-from tempest import config
from tempest.lib.common.utils import data_utils
from tempest.lib.common.utils import test_utils
from patrole_tempest_plugin import rbac_utils
-CONF = config.CONF
-
class BaseV2ComputeRbacTest(rbac_utils.RbacUtilsMixin,
compute_base.BaseV2ComputeTest):
@classmethod
- def skip_checks(cls):
- super(BaseV2ComputeRbacTest, cls).skip_checks()
- cls.skip_rbac_checks()
-
- @classmethod
def setup_clients(cls):
super(BaseV2ComputeRbacTest, cls).setup_clients()
cls.setup_rbac_utils()
diff --git a/patrole_tempest_plugin/tests/api/compute/test_flavor_access_rbac.py b/patrole_tempest_plugin/tests/api/compute/test_flavor_access_rbac.py
index d6364c9..a99ddbd 100644
--- a/patrole_tempest_plugin/tests/api/compute/test_flavor_access_rbac.py
+++ b/patrole_tempest_plugin/tests/api/compute/test_flavor_access_rbac.py
@@ -13,8 +13,9 @@
# License for the specific language governing permissions and limitations
# under the License.
-from tempest import config
+import testtools
+from tempest import config
from tempest.lib.common.utils import test_utils
from tempest.lib import decorators
@@ -34,6 +35,8 @@
cls.public_flavor_id = CONF.compute.flavor_ref
cls.tenant_id = cls.os_primary.credentials.tenant_id
+ @testtools.skipIf(CONF.policy_feature_enabled.removed_nova_policies_stein,
+ "This API extension policy was removed in Stein")
@decorators.idempotent_id('a2bd3740-765d-4c95-ac98-9e027378c75e')
@rbac_rule_validation.action(
service="nova",
@@ -50,6 +53,8 @@
raise rbac_exceptions.RbacMalformedResponse(
attribute=expected_attr)
+ @testtools.skipIf(CONF.policy_feature_enabled.removed_nova_policies_stein,
+ "This API extension policy was removed in Stein")
@decorators.idempotent_id('dd388146-9750-4124-82ba-62deff1052bb')
@rbac_rule_validation.action(
service="nova",
diff --git a/patrole_tempest_plugin/tests/api/compute/test_flavor_rxtx_rbac.py b/patrole_tempest_plugin/tests/api/compute/test_flavor_rxtx_rbac.py
index fbc03cf..b4531af 100644
--- a/patrole_tempest_plugin/tests/api/compute/test_flavor_rxtx_rbac.py
+++ b/patrole_tempest_plugin/tests/api/compute/test_flavor_rxtx_rbac.py
@@ -13,6 +13,8 @@
# License for the specific language governing permissions and limitations
# under the License.
+import testtools
+
from tempest.common import utils
from tempest import config
from tempest.lib import decorators
@@ -33,6 +35,8 @@
msg = "os-flavor-rxtx extension not enabled."
raise cls.skipException(msg)
+ @testtools.skipIf(CONF.policy_feature_enabled.removed_nova_policies_stein,
+ "This API extension policy was removed in Stein")
@decorators.idempotent_id('5e1fd9f0-9a08-485a-ad9c-0fc66e4d64b7')
@rbac_rule_validation.action(
service="nova",
@@ -44,6 +48,8 @@
raise rbac_exceptions.RbacMalformedResponse(
attribute='rxtx_factor')
+ @testtools.skipIf(CONF.policy_feature_enabled.removed_nova_policies_stein,
+ "This API extension policy was removed in Stein")
@decorators.idempotent_id('70c55a07-c843-4627-a29d-ba78673c1e63')
@rbac_rule_validation.action(
service="nova",
diff --git a/patrole_tempest_plugin/tests/api/compute/test_images_rbac.py b/patrole_tempest_plugin/tests/api/compute/test_images_rbac.py
index f36b8ec..c988128 100644
--- a/patrole_tempest_plugin/tests/api/compute/test_images_rbac.py
+++ b/patrole_tempest_plugin/tests/api/compute/test_images_rbac.py
@@ -13,6 +13,8 @@
# License for the specific language governing permissions and limitations
# under the License.
+import testtools
+
from tempest.common import image as common_image
from tempest import config
from tempest.lib.common.utils import data_utils
@@ -20,6 +22,7 @@
from tempest.lib import decorators
from tempest.lib import exceptions as lib_exc
+from patrole_tempest_plugin import rbac_exceptions
from patrole_tempest_plugin import rbac_rule_validation
from patrole_tempest_plugin.tests.api.compute import rbac_base
@@ -245,18 +248,67 @@
# https://developer.openstack.org/api-ref/compute/#images-deprecated
max_microversion = '2.35'
+ @classmethod
+ def skip_checks(cls):
+ super(ImageSizeRbacTest, cls).skip_checks()
+ if not CONF.service_available.glance:
+ skip_msg = ("%s skipped as glance is not available" % cls.__name__)
+ raise cls.skipException(skip_msg)
+
+ @classmethod
+ def setup_clients(cls):
+ super(ImageSizeRbacTest, cls).setup_clients()
+ if CONF.image_feature_enabled.api_v2:
+ cls.glance_image_client = cls.os_primary.image_client_v2
+ elif CONF.image_feature_enabled.api_v1:
+ cls.glance_image_client = cls.os_primary.image_client
+ else:
+ raise lib_exc.InvalidConfiguration(
+ 'Either api_v1 or api_v2 must be True in '
+ '[image-feature-enabled].')
+
+ @classmethod
+ def resource_setup(cls):
+ super(ImageSizeRbacTest, cls).resource_setup()
+ params = {'name': data_utils.rand_name(cls.__name__ + '-image')}
+ if CONF.image_feature_enabled.api_v1:
+ params = {'headers': common_image.image_meta_to_headers(**params)}
+
+ cls.image = cls.glance_image_client.create_image(**params)
+ cls.addClassResourceCleanup(
+ cls.glance_image_client.wait_for_resource_deletion,
+ cls.image['id'])
+ cls.addClassResourceCleanup(
+ cls.glance_image_client.delete_image, cls.image['id'])
+
+ @testtools.skipIf(CONF.policy_feature_enabled.removed_nova_policies_stein,
+ "This API extension policy was removed in Stein")
@decorators.idempotent_id('fe34d2a6-5743-45bf-8f92-a1d703d7c7ab')
@rbac_rule_validation.action(
service="nova",
rule="os_compute_api:image-size")
- def test_list_images(self):
+ def test_show_image_includes_image_size(self):
with self.rbac_utils.override_role(self):
- self.compute_images_client.list_images()
+ body = self.compute_images_client.show_image(self.image['id'])[
+ 'image']
+ expected_attr = 'OS-EXT-IMG-SIZE:size'
+ if expected_attr not in body:
+ raise rbac_exceptions.RbacMalformedResponse(
+ attribute=expected_attr)
+
+ @testtools.skipIf(CONF.policy_feature_enabled.removed_nova_policies_stein,
+ "This API extension policy was removed in Stein")
@decorators.idempotent_id('08342c7d-297d-42ee-b398-90fce2443792')
@rbac_rule_validation.action(
service="nova",
rule="os_compute_api:image-size")
- def test_list_images_with_details(self):
+ def test_list_images_with_details_includes_image_size(self):
with self.rbac_utils.override_role(self):
- self.compute_images_client.list_images(detail=True)
+ body = self.compute_images_client.list_images(detail=True)[
+ 'images']
+
+ expected_attr = 'OS-EXT-IMG-SIZE:size'
+ if expected_attr not in body[0]:
+ raise rbac_exceptions.RbacMalformedResponse(
+ attribute=expected_attr)
diff --git a/patrole_tempest_plugin/tests/api/compute/test_server_misc_policy_actions_rbac.py b/patrole_tempest_plugin/tests/api/compute/test_server_misc_policy_actions_rbac.py
index d97f382..5681799 100644
--- a/patrole_tempest_plugin/tests/api/compute/test_server_misc_policy_actions_rbac.py
+++ b/patrole_tempest_plugin/tests/api/compute/test_server_misc_policy_actions_rbac.py
@@ -129,6 +129,8 @@
waiters.wait_for_server_status(
self.servers_client, self.server['id'], 'ACTIVE')
+ @testtools.skipIf(CONF.policy_feature_enabled.removed_nova_policies_stein,
+ "This API extension policy was removed in Stein")
@utils.requires_ext(extension='os-config-drive', service='compute')
@decorators.idempotent_id('2c82e819-382d-4d6f-87f0-a45954cbbc64')
@rbac_rule_validation.action(
@@ -144,6 +146,8 @@
raise rbac_exceptions.RbacMalformedResponse(
attribute=expected_attr)
+ @testtools.skipIf(CONF.policy_feature_enabled.removed_nova_policies_stein,
+ "This API extension policy was removed in Stein")
@utils.requires_ext(extension='os-config-drive', service='compute')
@decorators.idempotent_id('55c62ef7-b72b-4970-acc6-05b0a4316e5d')
@rbac_rule_validation.action(
@@ -169,6 +173,8 @@
# Force-deleting a server enforces os-deferred-delete.
self.servers_client.force_delete_server(self.server['id'])
+ @testtools.skipIf(CONF.policy_feature_enabled.removed_nova_policies_stein,
+ "This API extension policy was removed in Stein")
@decorators.idempotent_id('d873740a-7b10-40a9-943d-7cc18115370e')
@utils.requires_ext(extension='OS-EXT-AZ', service='compute')
@rbac_rule_validation.action(
@@ -185,6 +191,8 @@
raise rbac_exceptions.RbacMalformedResponse(
attribute=expected_attr)
+ @testtools.skipIf(CONF.policy_feature_enabled.removed_nova_policies_stein,
+ "This API extension policy was removed in Stein")
@decorators.idempotent_id('727e5360-770a-4b9c-8015-513a40216635')
@utils.requires_ext(extension='OS-EXT-AZ', service='compute')
@rbac_rule_validation.action(
@@ -200,6 +208,8 @@
raise rbac_exceptions.RbacMalformedResponse(
attribute=expected_attr)
+ @testtools.skipIf(CONF.policy_feature_enabled.removed_nova_policies_stein,
+ "This API extension policy was removed in Stein")
@decorators.idempotent_id('4aa5d93e-4887-468a-8eb4-b6eca0ca6437')
@utils.requires_ext(extension='OS-EXT-SRV-ATTR', service='compute')
@rbac_rule_validation.action(
@@ -222,6 +232,8 @@
raise rbac_exceptions.RbacMalformedResponse(
attribute=whole_attr)
+ @testtools.skipIf(CONF.policy_feature_enabled.removed_nova_policies_stein,
+ "This API extension policy was removed in Stein")
@decorators.idempotent_id('2ed7aee2-94b2-4a9f-ae63-a51b7f94fe30')
@utils.requires_ext(extension='OS-EXT-SRV-ATTR', service='compute')
@rbac_rule_validation.action(
@@ -244,6 +256,8 @@
raise rbac_exceptions.RbacMalformedResponse(
attribute=whole_attr)
+ @testtools.skipIf(CONF.policy_feature_enabled.removed_nova_policies_stein,
+ "This API extension policy was removed in Stein")
@decorators.idempotent_id('82053c27-3134-4003-9b55-bc9fafdb0e3b')
@utils.requires_ext(extension='OS-EXT-STS', service='compute')
@rbac_rule_validation.action(
@@ -261,6 +275,8 @@
raise rbac_exceptions.RbacMalformedResponse(
attribute=attr)
+ @testtools.skipIf(CONF.policy_feature_enabled.removed_nova_policies_stein,
+ "This API extension policy was removed in Stein")
@decorators.idempotent_id('7d2620a5-eea1-4a8b-96ea-86ad77a73fc8')
@utils.requires_ext(extension='OS-EXT-STS', service='compute')
@rbac_rule_validation.action(
@@ -278,6 +294,8 @@
raise rbac_exceptions.RbacMalformedResponse(
attribute=attr)
+ @testtools.skipIf(CONF.policy_feature_enabled.removed_nova_policies_stein,
+ "This API extension policy was removed in Stein")
@decorators.idempotent_id('21e39cbe-6c32-48fc-80dd-3e1fece6053f')
@utils.requires_ext(extension='os-extended-volumes', service='compute')
@rbac_rule_validation.action(
@@ -295,6 +313,8 @@
raise rbac_exceptions.RbacMalformedResponse(
attribute=expected_attr)
+ @testtools.skipIf(CONF.policy_feature_enabled.removed_nova_policies_stein,
+ "This API extension policy was removed in Stein")
@decorators.idempotent_id('7f163708-0d25-4138-8512-dfdd72a92989')
@utils.requires_ext(extension='os-extended-volumes', service='compute')
@rbac_rule_validation.action(
@@ -348,6 +368,8 @@
raise rbac_exceptions.RbacMalformedResponse(
attribute='events.traceback')
+ @testtools.skipIf(CONF.policy_feature_enabled.removed_nova_policies_stein,
+ "This API extension policy was removed in Stein")
@rbac_rule_validation.action(
service="nova",
rule="os_compute_api:os-keypairs")
@@ -360,6 +382,8 @@
raise rbac_exceptions.RbacMalformedResponse(
attribute='key_name')
+ @testtools.skipIf(CONF.policy_feature_enabled.removed_nova_policies_stein,
+ "This API extension policy was removed in Stein")
@rbac_rule_validation.action(
service="nova",
rule="os_compute_api:os-keypairs")
@@ -469,6 +493,8 @@
with self.rbac_utils.override_role(self):
self.servers_client.show_password(self.server['id'])
+ @testtools.skipIf(CONF.policy_feature_enabled.removed_nova_policies_stein,
+ "This API extension policy was removed in Stein")
@utils.requires_ext(extension='OS-SRV-USG', service='compute')
@rbac_rule_validation.action(
service="nova",
diff --git a/patrole_tempest_plugin/tests/api/identity/rbac_base.py b/patrole_tempest_plugin/tests/api/identity/rbac_base.py
index 91b3d1e..44f5962 100644
--- a/patrole_tempest_plugin/tests/api/identity/rbac_base.py
+++ b/patrole_tempest_plugin/tests/api/identity/rbac_base.py
@@ -16,13 +16,11 @@
from oslo_log import log as logging
from tempest.api.identity import base
-from tempest import config
from tempest.lib.common.utils import data_utils
from tempest.lib.common.utils import test_utils
from patrole_tempest_plugin import rbac_utils
-CONF = config.CONF
LOG = logging.getLogger(__name__)
@@ -30,11 +28,6 @@
base.BaseIdentityTest):
@classmethod
- def skip_checks(cls):
- super(BaseIdentityRbacTest, cls).skip_checks()
- cls.skip_rbac_checks()
-
- @classmethod
def setup_clients(cls):
super(BaseIdentityRbacTest, cls).setup_clients()
cls.setup_rbac_utils()
diff --git a/patrole_tempest_plugin/tests/api/identity/v3/test_tokens_negative_rbac.py b/patrole_tempest_plugin/tests/api/identity/v3/test_tokens_negative_rbac.py
index 00d522c..da5d4cd 100644
--- a/patrole_tempest_plugin/tests/api/identity/v3/test_tokens_negative_rbac.py
+++ b/patrole_tempest_plugin/tests/api/identity/v3/test_tokens_negative_rbac.py
@@ -13,7 +13,6 @@
# License for the specific language governing permissions and limitations
# under the License.
-from tempest import config
from tempest.lib import decorators
from tempest.lib import exceptions as lib_exc
@@ -21,8 +20,6 @@
from patrole_tempest_plugin import rbac_utils
from patrole_tempest_plugin.tests.api.identity import rbac_base
-CONF = config.CONF
-
class IdentityTokenV3RbacTest(rbac_base.BaseIdentityV3RbacTest):
diff --git a/patrole_tempest_plugin/tests/api/image/rbac_base.py b/patrole_tempest_plugin/tests/api/image/rbac_base.py
index 954790d..becd564 100644
--- a/patrole_tempest_plugin/tests/api/image/rbac_base.py
+++ b/patrole_tempest_plugin/tests/api/image/rbac_base.py
@@ -12,22 +12,14 @@
# under the License.
from tempest.api.image import base as image_base
-from tempest import config
from patrole_tempest_plugin import rbac_utils
-CONF = config.CONF
-
class BaseV2ImageRbacTest(rbac_utils.RbacUtilsMixin,
image_base.BaseV2ImageTest):
@classmethod
- def skip_checks(cls):
- super(BaseV2ImageRbacTest, cls).skip_checks()
- cls.skip_rbac_checks()
-
- @classmethod
def setup_clients(cls):
super(BaseV2ImageRbacTest, cls).setup_clients()
cls.setup_rbac_utils()
diff --git a/patrole_tempest_plugin/tests/api/image/test_images_member_rbac.py b/patrole_tempest_plugin/tests/api/image/test_images_member_rbac.py
index 952c41f..4b5fd08 100644
--- a/patrole_tempest_plugin/tests/api/image/test_images_member_rbac.py
+++ b/patrole_tempest_plugin/tests/api/image/test_images_member_rbac.py
@@ -68,8 +68,8 @@
self.alt_tenant_id)
@rbac_rule_validation.action(service="glance",
- rule="get_member",
- expected_error_code=404)
+ rules=["get_member"],
+ expected_error_codes=[404])
@decorators.idempotent_id('c01fd308-6484-11e6-881e-080027d0d606')
def test_show_image_member(self):
diff --git a/patrole_tempest_plugin/tests/api/network/README.rst b/patrole_tempest_plugin/tests/api/network/README.rst
new file mode 100644
index 0000000..20d6196
--- /dev/null
+++ b/patrole_tempest_plugin/tests/api/network/README.rst
@@ -0,0 +1,49 @@
+.. _network-rbac-tests:
+
+Network RBAC Tests
+==================
+
+What are these tests?
+---------------------
+
+These tests are RBAC tests for Neutron and its associated plugins. They are
+broken up into the following categories:
+
+* :ref:`neutron-rbac-tests`
+* :ref:`neutron-plugin-rbac-tests`
+
+.. _neutron-rbac-tests:
+
+Neutron tests
+^^^^^^^^^^^^^
+
+Neutron RBAC tests inherit from the base class ``BaseNetworkRbacTest``. They
+test many of the Neutron policies found in the service's `policy.json file`_.
+These tests are gated in many `Zuul jobs`_ (master, n-1, n-2) against many
+roles (member, admin).
+
+.. _neutron-plugin-rbac-tests:
+
+Neutron plugin tests
+^^^^^^^^^^^^^^^^^^^^
+
+The Neutron RBAC plugin tests focus on testing RBAC for various Neutron
+extensions and plugins, or, stated differently:
+
+* tests that rely on `neutron-tempest-plugin`_
+* external Neutron plugins
+
+These tests inherit from the base class ``BaseNetworkPluginRbacTest``. If an
+extension or plugin is not enabled in the cloud, the corresponding tests are
+gracefully skipped.
+
+.. note::
+
+ Patrole should import as few dependencies from ``neutron_tempest_plugin`` as
+ possible (such as ``neutron_tempest_plugin.api.clients`` for the service
+ clients) because the module is not a `stable interface`_.
+
+.. _policy.json file: https://github.com/openstack/neutron/blob/master/etc/policy.json
+.. _Zuul jobs: https://github.com/openstack/patrole/blob/master/.zuul.yaml
+.. _neutron-tempest-plugin: https://github.com/openstack/neutron-tempest-plugin
+.. _stable interface: https://github.com/openstack/neutron-tempest-plugin/tree/master/neutron_tempest_plugin#warning
diff --git a/patrole_tempest_plugin/tests/api/network/rbac_base.py b/patrole_tempest_plugin/tests/api/network/rbac_base.py
index 9d3e28b..6102347 100644
--- a/patrole_tempest_plugin/tests/api/network/rbac_base.py
+++ b/patrole_tempest_plugin/tests/api/network/rbac_base.py
@@ -14,22 +14,14 @@
# under the License.
from tempest.api.network import base as network_base
-from tempest import config
from patrole_tempest_plugin import rbac_utils
-CONF = config.CONF
-
class BaseNetworkRbacTest(rbac_utils.RbacUtilsMixin,
network_base.BaseNetworkTest):
@classmethod
- def skip_checks(cls):
- super(BaseNetworkRbacTest, cls).skip_checks()
- cls.skip_rbac_checks()
-
- @classmethod
def setup_clients(cls):
super(BaseNetworkRbacTest, cls).setup_clients()
cls.setup_rbac_utils()
diff --git a/patrole_tempest_plugin/tests/api/network/test_address_scope_rbac.py b/patrole_tempest_plugin/tests/api/network/test_address_scope_rbac.py
new file mode 100644
index 0000000..cf73669
--- /dev/null
+++ b/patrole_tempest_plugin/tests/api/network/test_address_scope_rbac.py
@@ -0,0 +1,139 @@
+# 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.common import utils
+from tempest.lib.common.utils import data_utils
+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 AddressScopeRbacTest(base.BaseNetworkPluginRbacTest):
+
+ @classmethod
+ def skip_checks(cls):
+ super(AddressScopeRbacTest, cls).skip_checks()
+ if not utils.is_extension_enabled('address-scope', 'network'):
+ msg = "address-scope extension not enabled."
+ raise cls.skipException(msg)
+
+ @classmethod
+ def resource_setup(cls):
+ super(AddressScopeRbacTest, cls).resource_setup()
+ cls.network = cls.create_network()
+
+ def _create_address_scope(self, name=None, **kwargs):
+ name = name or data_utils.rand_name(self.__class__.__name__)
+ address_scope = self.ntp_client.create_address_scope(name=name,
+ ip_version=6,
+ **kwargs)
+ address_scope = address_scope['address_scope']
+ self.addCleanup(test_utils.call_and_ignore_notfound_exc,
+ self.ntp_client.delete_address_scope,
+ address_scope['id'])
+ return address_scope
+
+ @rbac_rule_validation.action(service="neutron",
+ rules=["create_address_scope"],
+ expected_error_codes=[403])
+ @decorators.idempotent_id('8cb2d6b5-23c2-4648-997b-7a6ae55be3ad')
+ def test_create_address_scope(self):
+
+ """Create Address Scope
+
+ RBAC test for the neutron create_address_scope policy
+ """
+ with self.rbac_utils.override_role(self):
+ self._create_address_scope()
+
+ @rbac_rule_validation.action(service="neutron",
+ rules=["create_address_scope",
+ "create_address_scope:shared"],
+ expected_error_codes=[403, 403])
+ @decorators.idempotent_id('0c3f55c0-6ebe-4251-afca-62c5cb4632ca')
+ def test_create_address_scope_shared(self):
+
+ """Create Shared Address Scope
+
+ RBAC test for the neutron create_address_scope:shared policy
+ """
+ with self.rbac_utils.override_role(self):
+ self._create_address_scope(shared=True)
+
+ @rbac_rule_validation.action(service="neutron",
+ rules=["get_address_scope"],
+ expected_error_codes=[404])
+ @decorators.idempotent_id('a53f741b-46f6-412f-936f-ac920d449da8')
+ def test_get_address_scope(self):
+
+ """Get Address Scope
+
+ RBAC test for the neutron get_address_scope policy
+ """
+ address_scope = self._create_address_scope()
+ with self.rbac_utils.override_role(self):
+ self.ntp_client.show_address_scope(address_scope['id'])
+
+ @rbac_rule_validation.action(service="neutron",
+ rules=["get_address_scope",
+ "update_address_scope"],
+ expected_error_codes=[404, 403])
+ @decorators.idempotent_id('3ce4d606-e067-4ef5-840f-96c680226e73')
+ def test_update_address_scope(self):
+
+ """Update Address Scope
+
+ RBAC test for neutron update_address_scope policy
+ """
+ address_scope = self._create_address_scope()
+ name = data_utils.rand_name(self.__class__.__name__)
+ with self.rbac_utils.override_role(self):
+ self.ntp_client.update_address_scope(address_scope['id'],
+ name=name)
+
+ @rbac_rule_validation.action(service="neutron",
+ rules=["get_address_scope",
+ "update_address_scope",
+ "update_address_scope:shared"],
+ expected_error_codes=[404, 403, 403])
+ @decorators.idempotent_id('77d3a9d2-721a-4d9f-9654-6b52f113df85')
+ def test_update_address_scope_shared(self):
+
+ """Update Shared Address Scope
+
+ RBAC test for neutron update_address_scope:shared policy
+ """
+ address_scope = self._create_address_scope(shared=True)
+ with self.rbac_utils.override_role(self):
+ self.ntp_client.update_address_scope(address_scope['id'],
+ shared=False)
+
+ @rbac_rule_validation.action(service="neutron",
+ rules=["get_address_scope",
+ "delete_address_scope"],
+ expected_error_codes=[404, 403])
+ @decorators.idempotent_id('277d8e47-e498-4452-b969-a91f747296ba')
+ def test_delete_address_scope(self):
+
+ """Delete Address Scope
+
+ RBAC test for neutron delete_address_scope policy
+ """
+ address_scope = self._create_address_scope()
+ with self.rbac_utils.override_role(self):
+ self.ntp_client.delete_address_scope(address_scope['id'])
diff --git a/patrole_tempest_plugin/tests/api/network/test_agents_rbac.py b/patrole_tempest_plugin/tests/api/network/test_agents_rbac.py
index 2756a10..7567275 100644
--- a/patrole_tempest_plugin/tests/api/network/test_agents_rbac.py
+++ b/patrole_tempest_plugin/tests/api/network/test_agents_rbac.py
@@ -38,8 +38,8 @@
@decorators.idempotent_id('f88e38e0-ab52-4b97-8ffa-48a27f9d199b')
@rbac_rule_validation.action(service="neutron",
- rule="get_agent",
- expected_error_code=404)
+ rules=["get_agent"],
+ expected_error_codes=[404])
def test_show_agent(self):
"""Show agent test.
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 cdc9852..f8ef0bb 100644
--- a/patrole_tempest_plugin/tests/api/network/test_flavors_rbac.py
+++ b/patrole_tempest_plugin/tests/api/network/test_flavors_rbac.py
@@ -13,6 +13,8 @@
# 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
@@ -116,3 +118,70 @@
with self.rbac_utils.override_role(self):
self.ntp_client.list_flavors()
+
+
+class FlavorsServiceProfilePluginRbacTest(base.BaseNetworkPluginRbacTest):
+ @classmethod
+ def resource_setup(cls):
+ super(FlavorsServiceProfilePluginRbacTest, 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=["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_floating_ips_rbac.py b/patrole_tempest_plugin/tests/api/network/test_floating_ips_rbac.py
index ed52c34..8a02149 100644
--- a/patrole_tempest_plugin/tests/api/network/test_floating_ips_rbac.py
+++ b/patrole_tempest_plugin/tests/api/network/test_floating_ips_rbac.py
@@ -76,8 +76,7 @@
@rbac_rule_validation.action(
service="neutron",
rules=["create_floatingip",
- "create_floatingip:floating_ip_address"],
- expected_error_codes=[403, 403])
+ "create_floatingip:floating_ip_address"])
@decorators.idempotent_id('a8bb826a-403d-4130-a55d-120a0a660806')
def test_create_floating_ip_floatingip_address(self):
"""Create floating IP with address.
@@ -105,8 +104,8 @@
floating_ip['id'], port_id=None)
@rbac_rule_validation.action(service="neutron",
- rule="get_floatingip",
- expected_error_code=404)
+ rules=["get_floatingip"],
+ expected_error_codes=[404])
@decorators.idempotent_id('f8846fd0-c976-48fe-a148-105303931b32')
def test_show_floating_ip(self):
"""Show floating IP.
diff --git a/patrole_tempest_plugin/tests/api/network/test_metering_label_rules_rbac.py b/patrole_tempest_plugin/tests/api/network/test_metering_label_rules_rbac.py
index adab1e6..db099a1 100644
--- a/patrole_tempest_plugin/tests/api/network/test_metering_label_rules_rbac.py
+++ b/patrole_tempest_plugin/tests/api/network/test_metering_label_rules_rbac.py
@@ -74,8 +74,8 @@
self._create_metering_label_rule(self.label)
@rbac_rule_validation.action(service="neutron",
- rule="get_metering_label_rule",
- expected_error_code=404)
+ rules=["get_metering_label_rule"],
+ expected_error_codes=[404])
@decorators.idempotent_id('e21b40c3-d44d-412f-84ea-836ca8603bcb')
def test_show_metering_label_rule(self):
"""Show metering label rule.
diff --git a/patrole_tempest_plugin/tests/api/network/test_metering_labels_rbac.py b/patrole_tempest_plugin/tests/api/network/test_metering_labels_rbac.py
index 0231868..0e10f5b 100644
--- a/patrole_tempest_plugin/tests/api/network/test_metering_labels_rbac.py
+++ b/patrole_tempest_plugin/tests/api/network/test_metering_labels_rbac.py
@@ -58,8 +58,8 @@
self._create_metering_label()
@rbac_rule_validation.action(service="neutron",
- rule="get_metering_label",
- expected_error_code=404)
+ rules=["get_metering_label"],
+ expected_error_codes=[404])
@decorators.idempotent_id('c57f6636-c702-4755-8eac-5e73bc1f7d14')
def test_show_metering_label(self):
"""Show metering label.
diff --git a/patrole_tempest_plugin/tests/api/network/test_network_segments_rbac.py b/patrole_tempest_plugin/tests/api/network/test_network_segments_rbac.py
index 0097c7b..c985111 100644
--- a/patrole_tempest_plugin/tests/api/network/test_network_segments_rbac.py
+++ b/patrole_tempest_plugin/tests/api/network/test_network_segments_rbac.py
@@ -67,8 +67,7 @@
@rbac_rule_validation.action(service="neutron",
rules=["create_network",
- "create_network:segments"],
- expected_error_codes=[403, 403])
+ "create_network:segments"])
@decorators.idempotent_id('9e1d0c3d-92e3-40e3-855e-bfbb72ea6e0b')
def test_create_network_segments(self):
"""Create network with segments.
diff --git a/patrole_tempest_plugin/tests/api/network/test_networks_rbac.py b/patrole_tempest_plugin/tests/api/network/test_networks_rbac.py
index 72674f6..2e69f89 100644
--- a/patrole_tempest_plugin/tests/api/network/test_networks_rbac.py
+++ b/patrole_tempest_plugin/tests/api/network/test_networks_rbac.py
@@ -110,8 +110,7 @@
@rbac_rule_validation.action(service="neutron",
rules=["create_network",
- "create_network:is_default"],
- expected_error_codes=[403, 403])
+ "create_network:is_default"])
@decorators.idempotent_id('28602661-5ac7-407e-b739-e393f619f5e3')
def test_create_network_is_default(self):
@@ -129,8 +128,7 @@
@rbac_rule_validation.action(service="neutron",
rules=["create_network",
- "create_network:shared"],
- expected_error_codes=[403, 403])
+ "create_network:shared"])
@decorators.idempotent_id('ccabf2a9-28c8-44b2-80e6-ffd65d43eef2')
def test_create_network_shared(self):
@@ -144,8 +142,7 @@
@utils.requires_ext(extension='external-net', service='network')
@rbac_rule_validation.action(service="neutron",
rules=["create_network",
- "create_network:router:external"],
- expected_error_codes=[403, 403])
+ "create_network:router:external"])
@decorators.idempotent_id('51adf2a7-739c-41e0-8857-3b4c460cbd24')
def test_create_network_router_external(self):
@@ -160,8 +157,7 @@
@rbac_rule_validation.action(
service="neutron",
rules=["create_network",
- "create_network:provider:physical_network"],
- expected_error_codes=[403, 403])
+ "create_network:provider:physical_network"])
@decorators.idempotent_id('76783fed-9ff3-4499-a0d1-82d99eec364e')
def test_create_network_provider_physical_network(self):
@@ -184,8 +180,7 @@
@rbac_rule_validation.action(
service="neutron",
rules=["create_network",
- "create_network:provider:network_type"],
- expected_error_codes=[403, 403])
+ "create_network:provider:network_type"])
@decorators.idempotent_id('3c42f7b8-b80c-44ef-8fa4-69ec4b1836bc')
def test_create_network_provider_network_type(self):
@@ -200,8 +195,7 @@
@rbac_rule_validation.action(
service="neutron",
rules=["create_network",
- "create_network:provider:segmentation_id"],
- expected_error_codes=[403, 403])
+ "create_network:provider:segmentation_id"])
@decorators.idempotent_id('b9decb7b-68ef-4504-b99b-41edbf7d2af5')
def test_create_network_provider_segmentation_id(self):
@@ -338,8 +332,8 @@
str(exc))
@rbac_rule_validation.action(service="neutron",
- rule="get_network",
- expected_error_code=404)
+ rules=["get_network"],
+ expected_error_codes=[404])
@decorators.idempotent_id('0eb62d04-338a-4ff4-a8fa-534e52110534')
def test_show_network(self):
diff --git a/patrole_tempest_plugin/tests/api/network/test_policy_minimum_bandwidth_rule_rbac.py b/patrole_tempest_plugin/tests/api/network/test_policy_minimum_bandwidth_rule_rbac.py
new file mode 100644
index 0000000..4f85cb6
--- /dev/null
+++ b/patrole_tempest_plugin/tests/api/network/test_policy_minimum_bandwidth_rule_rbac.py
@@ -0,0 +1,112 @@
+# Copyright 2017 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.common import utils
+from tempest.lib.common.utils import data_utils
+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 PolicyMinimumBandwidthRulePluginRbacTest(base.BaseNetworkPluginRbacTest):
+
+ @classmethod
+ def skip_checks(cls):
+ super(PolicyMinimumBandwidthRulePluginRbacTest, cls).skip_checks()
+ if not utils.is_extension_enabled('qos', 'network'):
+ msg = "qos extension not enabled."
+ raise cls.skipException(msg)
+
+ @classmethod
+ def resource_setup(cls):
+ super(PolicyMinimumBandwidthRulePluginRbacTest, cls).resource_setup()
+ name = data_utils.rand_name(cls.__class__.__name__ + '-qos')
+ cls.policy_id = cls.ntp_client.create_qos_policy(
+ name=name)["policy"]["id"]
+ cls.addClassResourceCleanup(test_utils.call_and_ignore_notfound_exc,
+ cls.ntp_client.delete_qos_policy,
+ cls.policy_id)
+
+ def create_minimum_bandwidth_rule(self):
+ rule = self.ntp_client.create_minimum_bandwidth_rule(
+ self.policy_id, direction="egress", min_kbps=1000)
+ rule_id = rule['minimum_bandwidth_rule']['id']
+ self.addCleanup(test_utils.call_and_ignore_notfound_exc,
+ self.ntp_client.delete_minimum_bandwidth_rule,
+ self.policy_id, rule_id)
+ return rule_id
+
+ @decorators.idempotent_id('25B5EF3A-DF2A-4C80-A498-3BE14A321D97')
+ @rbac_rule_validation.action(
+ service="neutron", rules=["create_policy_minimum_bandwidth_rule"])
+ def test_create_policy_minimum_bandwidth_rule(self):
+ """Create policy_minimum_bandwidth_rule.
+
+ RBAC test for the neutron "create_policy_minimum_bandwidth_rule" policy
+ """
+
+ with self.rbac_utils.override_role(self):
+ self.create_minimum_bandwidth_rule()
+
+ @decorators.idempotent_id('01DD902C-47C5-45D2-9A0E-7AF05981DF21')
+ @rbac_rule_validation.action(service="neutron",
+ rules=["get_policy_minimum_bandwidth_rule"],
+ expected_error_codes=[404])
+ def test_show_policy_minimum_bandwidth_rule(self):
+ """Show policy_minimum_bandwidth_rule.
+
+ RBAC test for the neutron "get_policy_minimum_bandwidth_rule" policy
+ """
+ rule_id = self.create_minimum_bandwidth_rule()
+
+ with self.rbac_utils.override_role(self):
+ self.ntp_client.show_minimum_bandwidth_rule(
+ self.policy_id, rule_id)
+
+ @decorators.idempotent_id('50AFE69B-455C-413A-BDC6-26B42DC8D55D')
+ @rbac_rule_validation.action(
+ service="neutron",
+ rules=["get_policy_minimum_bandwidth_rule",
+ "update_policy_minimum_bandwidth_rule"],
+ expected_error_codes=[404, 403])
+ def test_update_policy_minimum_bandwidth_rule(self):
+ """Update policy_minimum_bandwidth_rule.
+
+ RBAC test for the neutron "update_policy_minimum_bandwidth_rule" policy
+ """
+ rule_id = self.create_minimum_bandwidth_rule()
+
+ with self.rbac_utils.override_role(self):
+ self.ntp_client.update_minimum_bandwidth_rule(
+ self.policy_id, rule_id, min_kbps=2000)
+
+ @decorators.idempotent_id('2112E325-C3B2-4071-8A93-B218F275A83B')
+ @rbac_rule_validation.action(
+ service="neutron",
+ rules=["get_policy_minimum_bandwidth_rule",
+ "delete_policy_minimum_bandwidth_rule"],
+ expected_error_codes=[404, 403])
+ def test_delete_policy_minimum_bandwidth_rule(self):
+ """Delete policy_minimum_bandwidth_rule.
+
+ RBAC test for the neutron "delete_policy_minimum_bandwidth_rule" policy
+ """
+ rule_id = self.create_minimum_bandwidth_rule()
+
+ with self.rbac_utils.override_role(self):
+ self.ntp_client.delete_minimum_bandwidth_rule(
+ self.policy_id, rule_id)
diff --git a/patrole_tempest_plugin/tests/api/network/test_ports_rbac.py b/patrole_tempest_plugin/tests/api/network/test_ports_rbac.py
index 2cf3cd6..175d051 100644
--- a/patrole_tempest_plugin/tests/api/network/test_ports_rbac.py
+++ b/patrole_tempest_plugin/tests/api/network/test_ports_rbac.py
@@ -69,8 +69,7 @@
@decorators.idempotent_id('045ee797-4962-4913-b96a-5d7ea04099e7')
@rbac_rule_validation.action(service="neutron",
rules=["create_port",
- "create_port:device_owner"],
- expected_error_codes=[403, 403])
+ "create_port:device_owner"])
def test_create_port_device_owner(self):
with self.rbac_utils.override_role(self):
self.create_port(self.network,
@@ -79,8 +78,7 @@
@decorators.idempotent_id('c4fa8844-f5ef-4daa-bfa2-b89897dfaedf')
@rbac_rule_validation.action(service="neutron",
rules=["create_port",
- "create_port:port_security_enabled"],
- expected_error_codes=[403, 403])
+ "create_port:port_security_enabled"])
def test_create_port_security_enabled(self):
with self.rbac_utils.override_role(self):
self.create_port(self.network, port_security_enabled=True)
@@ -88,8 +86,7 @@
@utils.requires_ext(extension='binding', service='network')
@rbac_rule_validation.action(service="neutron",
rules=["create_port",
- "create_port:binding:host_id"],
- expected_error_codes=[403, 403])
+ "create_port:binding:host_id"])
@decorators.idempotent_id('a54bd6b8-a7eb-4101-bfe8-093930b0d660')
def test_create_port_binding_host_id(self):
@@ -102,8 +99,7 @@
@utils.requires_ext(extension='binding', service='network')
@rbac_rule_validation.action(service="neutron",
rules=["create_port",
- "create_port:binding:profile"],
- expected_error_codes=[403, 403])
+ "create_port:binding:profile"])
@decorators.idempotent_id('98fa38ab-c2ed-46a0-99f0-59f18cbd257a')
def test_create_port_binding_profile(self):
@@ -120,8 +116,7 @@
'"create_port:fixed_ips:ip_address" must be available in the cloud.')
@rbac_rule_validation.action(service="neutron",
rules=["create_port",
- "create_port:fixed_ips:ip_address"],
- expected_error_codes=[403, 403])
+ "create_port:fixed_ips:ip_address"])
@decorators.idempotent_id('2551e10d-006a-413c-925a-8c6f834c09ac')
def test_create_port_fixed_ips_ip_address(self):
@@ -137,8 +132,7 @@
@rbac_rule_validation.action(service="neutron",
rules=["create_port",
- "create_port:mac_address"],
- expected_error_codes=[403, 403])
+ "create_port:mac_address"])
@decorators.idempotent_id('aee6d0be-a7f3-452f-aefc-796b4eb9c9a8')
def test_create_port_mac_address(self):
@@ -150,8 +144,7 @@
@rbac_rule_validation.action(service="neutron",
rules=["create_port",
- "create_port:allowed_address_pairs"],
- expected_error_codes=[403, 403])
+ "create_port:allowed_address_pairs"])
@decorators.idempotent_id('b638d1f4-d903-4ca8-aa2a-6fd603c5ec3a')
def test_create_port_allowed_address_pairs(self):
@@ -166,8 +159,8 @@
self.create_port(**post_body)
@rbac_rule_validation.action(service="neutron",
- rule="get_port",
- expected_error_code=404)
+ rules=["get_port"],
+ expected_error_codes=[404])
@decorators.idempotent_id('a9d41cb8-78a2-4b97-985c-44e4064416f4')
def test_show_port(self):
with self.rbac_utils.override_role(self):
diff --git a/patrole_tempest_plugin/tests/api/network/test_qos_rbac.py b/patrole_tempest_plugin/tests/api/network/test_qos_rbac.py
new file mode 100644
index 0000000..20f9e61
--- /dev/null
+++ b/patrole_tempest_plugin/tests/api/network/test_qos_rbac.py
@@ -0,0 +1,100 @@
+# 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.common import utils
+from tempest.lib.common.utils import data_utils
+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 QosRbacTest(base.BaseNetworkPluginRbacTest):
+
+ @classmethod
+ def skip_checks(cls):
+ super(QosRbacTest, cls).skip_checks()
+ if not utils.is_extension_enabled('qos', 'network'):
+ msg = "qos extension not enabled."
+ raise cls.skipException(msg)
+
+ @classmethod
+ def resource_setup(cls):
+ super(QosRbacTest, cls).resource_setup()
+ cls.network = cls.create_network()
+
+ def create_policy(self, name=None):
+ name = name or data_utils.rand_name(self.__class__.__name__)
+ policy = self.ntp_client.create_qos_policy(name)['policy']
+ self.addCleanup(test_utils.call_and_ignore_notfound_exc,
+ self.ntp_client.delete_qos_policy, policy['id'])
+ return policy
+
+ @rbac_rule_validation.action(service="neutron",
+ rules=["create_policy"],
+ expected_error_codes=[403])
+ @decorators.idempotent_id('2ade2e48-7f82-4650-a69c-933d8d594636')
+ def test_create_policy(self):
+
+ """Create Policy Test
+
+ RBAC test for the neutron create_policy policy
+ """
+ with self.rbac_utils.override_role(self):
+ self.create_policy()
+
+ @rbac_rule_validation.action(service="neutron",
+ rules=["get_policy"],
+ expected_error_codes=[404])
+ @decorators.idempotent_id('d004a8de-b226-4eb4-9fdc-8202a7f64c56')
+ def test_get_policy(self):
+
+ """Show Policy Test
+
+ RBAC test for the neutron get_policy policy
+ """
+ policy = self.create_policy()
+ with self.rbac_utils.override_role(self):
+ self.ntp_client.show_qos_policy(policy['id'])
+
+ @rbac_rule_validation.action(service="neutron",
+ rules=["get_policy", "update_policy"],
+ expected_error_codes=[404, 403])
+ @decorators.idempotent_id('fb74d56f-1dfc-490b-a9e1-454af583eefb')
+ def test_update_policy(self):
+
+ """Update Policy Test
+
+ RBAC test for the neutron update_policy policy
+ """
+ policy = self.create_policy()
+ with self.rbac_utils.override_role(self):
+ self.ntp_client.update_qos_policy(policy['id'],
+ description='updated')
+
+ @rbac_rule_validation.action(service="neutron",
+ rules=["get_policy", "delete_policy"],
+ expected_error_codes=[404, 403])
+ @decorators.idempotent_id('ef4c23a6-4095-47a6-958e-1df585f7d8db')
+ def test_delete_policy(self):
+
+ """Delete Policy Test
+
+ RBAC test for the neutron delete_policy policy
+ """
+ policy = self.create_policy()
+ with self.rbac_utils.override_role(self):
+ self.ntp_client.delete_qos_policy(policy['id'])
diff --git a/patrole_tempest_plugin/tests/api/network/test_rbac_policies_rbac.py b/patrole_tempest_plugin/tests/api/network/test_rbac_policies_rbac.py
new file mode 100644
index 0000000..a8813e7
--- /dev/null
+++ b/patrole_tempest_plugin/tests/api/network/test_rbac_policies_rbac.py
@@ -0,0 +1,111 @@
+# 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 RbacPoliciesPluginRbacTest(base.BaseNetworkPluginRbacTest):
+
+ @classmethod
+ def resource_setup(cls):
+ super(RbacPoliciesPluginRbacTest, cls).resource_setup()
+ cls.tenant_id = cls.os_primary.credentials.tenant_id
+ cls.network_id = cls.create_network()['id']
+
+ def create_rbac_policy(self, tenant_id, network_id):
+ policy = self.ntp_client.create_rbac_policy(
+ target_tenant=self.tenant_id,
+ object_type="network",
+ object_id=self.network_id,
+ action="access_as_shared"
+ )
+ self.addCleanup(
+ test_utils.call_and_ignore_notfound_exc,
+ self.ntp_client.delete_rbac_policy, policy["rbac_policy"]["id"])
+
+ return policy["rbac_policy"]["id"]
+
+ @decorators.idempotent_id('effd9545-99ad-4c3c-92dd-ea422602c868')
+ @rbac_rule_validation.action(service="neutron",
+ rules=["create_rbac_policy",
+ "create_rbac_policy:target_tenant"])
+ def test_create_rbac_policy(self):
+ """Create RBAC policy.
+
+ RBAC test for the neutron "create_rbac_policy" policy
+
+ We can't validate "create_rbac_policy:target_tenant" for all cases
+ since if "restrict_wildcard" rule is modified then Patrole won't be
+ able to determine the correct result since that requires relying on
+ Neutron's custom FieldCheck oslo.policy rule.
+ """
+
+ with self.rbac_utils.override_role(self):
+ self.create_rbac_policy(self.tenant_id, self.network_id)
+
+ @decorators.idempotent_id('f5d836d8-3b64-412d-a283-ee29761017f3')
+ @rbac_rule_validation.action(service="neutron",
+ rules=["get_rbac_policy",
+ "update_rbac_policy",
+ "update_rbac_policy:target_tenant"],
+ expected_error_codes=[404, 403, 403])
+ def test_update_rbac_policy(self):
+ """Update RBAC policy.
+
+ RBAC test for the neutron "update_rbac_policy" policy
+
+ We can't validate "create_rbac_policy:target_tenant" for all cases
+ since if "restrict_wildcard" rule is modified then Patrole won't be
+ able to determine the correct result since that requires relying on
+ Neutron's custom FieldCheck oslo.policy rule.
+ """
+ policy_id = self.create_rbac_policy(self.tenant_id, self.network_id)
+
+ with self.rbac_utils.override_role(self):
+ self.ntp_client.update_rbac_policy(
+ policy_id, target_tenant=self.tenant_id)
+
+ @decorators.idempotent_id('9308ab18-426c-41b7-bce5-11081f7dd259')
+ @rbac_rule_validation.action(service="neutron",
+ rules=["get_rbac_policy"],
+ expected_error_codes=[404])
+ def test_show_rbac_policy(self):
+ """Show RBAC policy.
+
+ RBAC test for the neutron "get_rbac_policy" policy
+ """
+ policy_id = self.create_rbac_policy(self.tenant_id, self.network_id)
+
+ with self.rbac_utils.override_role(self):
+ self.ntp_client.show_rbac_policy(policy_id)
+
+ @decorators.idempotent_id('54aa9bce-efea-47fb-b0e4-12012f82f285')
+ @rbac_rule_validation.action(service="neutron",
+ rules=["get_rbac_policy",
+ "delete_rbac_policy"],
+ expected_error_codes=[404, 403])
+ def test_delete_rbac_policy(self):
+ """Delete RBAC policy.
+
+ RBAC test for the neutron "delete_rbac_policy" policy
+ """
+ policy_id = self.create_rbac_policy(self.tenant_id, self.network_id)
+
+ with self.rbac_utils.override_role(self):
+ self.ntp_client.delete_rbac_policy(policy_id)
diff --git a/patrole_tempest_plugin/tests/api/network/test_routers_rbac.py b/patrole_tempest_plugin/tests/api/network/test_routers_rbac.py
index a3d973d..3d7631a 100644
--- a/patrole_tempest_plugin/tests/api/network/test_routers_rbac.py
+++ b/patrole_tempest_plugin/tests/api/network/test_routers_rbac.py
@@ -72,8 +72,7 @@
@utils.requires_ext(extension='l3-ha', service='network')
@rbac_rule_validation.action(service="neutron",
rules=["create_router",
- "create_router:ha"],
- expected_error_codes=[403, 403])
+ "create_router:ha"])
def test_create_high_availability_router(self):
"""Create high-availability router
@@ -88,8 +87,7 @@
@utils.requires_ext(extension='dvr', service='network')
@rbac_rule_validation.action(service="neutron",
rules=["create_router",
- "create_router:distributed"],
- expected_error_codes=[403, 403])
+ "create_router:distributed"])
def test_create_distributed_router(self):
"""Create distributed router
@@ -104,8 +102,7 @@
@rbac_rule_validation.action(
service="neutron",
rules=["create_router",
- "create_router:external_gateway_info:enable_snat"],
- expected_error_codes=[403, 403])
+ "create_router:external_gateway_info:enable_snat"])
@decorators.idempotent_id('3c5acd49-0ec7-4109-ab51-640557b48ebc')
def test_create_router_enable_snat(self):
"""Create Router Snat
@@ -126,8 +123,7 @@
@rbac_rule_validation.action(
service="neutron",
rules=["create_router",
- "create_router:external_gateway_info:external_fixed_ips"],
- expected_error_codes=[403, 403])
+ "create_router:external_gateway_info:external_fixed_ips"])
@decorators.idempotent_id('d0354369-a040-4349-b869-645c8aed13cd')
def test_create_router_external_fixed_ips(self):
"""Create Router Fixed IPs
@@ -151,8 +147,8 @@
router['router']['id'])
@rbac_rule_validation.action(service="neutron",
- rule="get_router",
- expected_error_code=404)
+ rules=["get_router"],
+ expected_error_codes=[404])
@decorators.idempotent_id('bfbdbcff-f115-4d3e-8cd5-6ada33fd0e21')
def test_show_router(self):
"""Get Router
diff --git a/patrole_tempest_plugin/tests/api/network/test_security_groups_rbac.py b/patrole_tempest_plugin/tests/api/network/test_security_groups_rbac.py
index 1cf841d..4536fdb 100644
--- a/patrole_tempest_plugin/tests/api/network/test_security_groups_rbac.py
+++ b/patrole_tempest_plugin/tests/api/network/test_security_groups_rbac.py
@@ -78,8 +78,8 @@
self._create_security_group()
@rbac_rule_validation.action(service="neutron",
- rule="get_security_group",
- expected_error_code=404)
+ rules=["get_security_group"],
+ expected_error_codes=[404])
@decorators.idempotent_id('56335e77-aef2-4b54-86c7-7f772034b585')
def test_show_security_group(self):
@@ -149,8 +149,8 @@
sec_group_rule['id'])
@rbac_rule_validation.action(service="neutron",
- rule="get_security_group_rule",
- expected_error_code=404)
+ rules=["get_security_group_rule"],
+ expected_error_codes=[404])
@decorators.idempotent_id('84b4038c-261e-4a94-90d5-c885739ab0d5')
def test_show_security_group_rule(self):
diff --git a/patrole_tempest_plugin/tests/api/network/test_subnetpools_rbac.py b/patrole_tempest_plugin/tests/api/network/test_subnetpools_rbac.py
index 124b59a..62735d7 100644
--- a/patrole_tempest_plugin/tests/api/network/test_subnetpools_rbac.py
+++ b/patrole_tempest_plugin/tests/api/network/test_subnetpools_rbac.py
@@ -65,6 +65,31 @@
@rbac_rule_validation.action(service="neutron",
rules=["create_subnetpool",
+ "create_subnetpool:is_default"],
+ expected_error_codes=[403, 403])
+ @decorators.idempotent_id('1b5509fd-2c32-44a8-a786-1b6ca162dbd2')
+ def test_create_subnetpool_default(self):
+ """Create default subnetpool.
+
+ RBAC test for the neutron create_subnetpool:is_default policy
+ """
+ # Most likely we already have default subnetpools for ipv4 and ipv6,
+ # so we temporary mark them as is_default=False, to let this test pass.
+ def_pools = self.subnetpools_client.list_subnetpools(is_default=True)
+ for default_pool in def_pools["subnetpools"]:
+ self.subnetpools_client.update_subnetpool(default_pool["id"],
+ is_default=False)
+
+ self.addCleanup(self.subnetpools_client.update_subnetpool,
+ default_pool["id"], is_default=True)
+
+ with self.rbac_utils.override_role(self):
+ # It apparently only enforces the policy for is_default=True.
+ # It does nothing for is_default=False
+ self._create_subnetpool(is_default=True)
+
+ @rbac_rule_validation.action(service="neutron",
+ rules=["create_subnetpool",
"create_subnetpool:shared"],
expected_error_codes=[403, 403])
@decorators.idempotent_id('cf730989-0d47-40bc-b39a-99e7de484723')
@@ -77,8 +102,8 @@
self._create_subnetpool(shared=True)
@rbac_rule_validation.action(service="neutron",
- rule="get_subnetpool",
- expected_error_code=404)
+ rules=["get_subnetpool"],
+ expected_error_codes=[404])
@decorators.idempotent_id('4f5aee26-0507-4b6d-b44c-3128a25094d2')
def test_show_subnetpool(self):
"""Show subnetpool.
@@ -107,8 +132,7 @@
@decorators.idempotent_id('a16f4e5c-0675-415f-b636-00af00638693')
@rbac_rule_validation.action(service="neutron",
rules=["update_subnetpool",
- "update_subnetpool:is_default"],
- expected_error_codes=[403, 403])
+ "update_subnetpool:is_default"])
def test_update_subnetpool_is_default(self):
"""Update default subnetpool.
diff --git a/patrole_tempest_plugin/tests/api/network/test_subnets_rbac.py b/patrole_tempest_plugin/tests/api/network/test_subnets_rbac.py
index 77d4b42..93d79a9 100644
--- a/patrole_tempest_plugin/tests/api/network/test_subnets_rbac.py
+++ b/patrole_tempest_plugin/tests/api/network/test_subnets_rbac.py
@@ -50,8 +50,8 @@
@decorators.idempotent_id('c02618e7-bb20-4abd-83c8-6eec2af08752')
@rbac_rule_validation.action(service="neutron",
- rule="get_subnet",
- expected_error_code=404)
+ rules=["get_subnet"],
+ expected_error_codes=[404])
def test_show_subnet(self):
"""Show subnet.
diff --git a/patrole_tempest_plugin/tests/api/network/test_trunks_rbac.py b/patrole_tempest_plugin/tests/api/network/test_trunks_rbac.py
new file mode 100644
index 0000000..063fd55
--- /dev/null
+++ b/patrole_tempest_plugin/tests/api/network/test_trunks_rbac.py
@@ -0,0 +1,85 @@
+# 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.common import utils
+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 TrunksPluginRbacTest(base.BaseNetworkPluginRbacTest):
+
+ @classmethod
+ def skip_checks(cls):
+ super(TrunksPluginRbacTest, cls).skip_checks()
+ if not utils.is_extension_enabled('trunk', 'network'):
+ msg = "trunk extension not enabled."
+ raise cls.skipException(msg)
+
+ @classmethod
+ def resource_setup(cls):
+ super(TrunksPluginRbacTest, cls).resource_setup()
+ cls.network = cls.create_network()
+ cls.port_id = cls.create_port(cls.network)["id"]
+
+ def create_trunk(self, port_id):
+ trunk = self.ntp_client.create_trunk(port_id, [])
+ self.addCleanup(
+ test_utils.call_and_ignore_notfound_exc,
+ self.ntp_client.delete_trunk, trunk["trunk"]['id'])
+
+ return trunk
+
+ @decorators.idempotent_id('c02618e7-bb20-1a3a-83c8-6eec2af08130')
+ @rbac_rule_validation.action(service="neutron",
+ rules=["create_trunk"])
+ def test_create_trunk(self):
+ """Create trunk.
+
+ RBAC test for the neutron "create_trunk" policy
+ """
+ with self.rbac_utils.override_role(self):
+ self.create_trunk(self.port_id)
+
+ @decorators.idempotent_id('c02618e7-bb20-1a3a-83c8-6eec2af08131')
+ @rbac_rule_validation.action(service="neutron",
+ rules=["get_trunk"],
+ expected_error_codes=[404])
+ def test_show_trunk(self):
+ """Show trunk.
+
+ RBAC test for the neutron "get_trunk" policy
+ """
+ trunk = self.create_trunk(self.port_id)
+
+ with self.rbac_utils.override_role(self):
+ self.ntp_client.show_trunk(trunk['trunk']['id'])
+
+ @decorators.idempotent_id('c02618e7-bb20-1a3a-83c8-6eec2af08132')
+ @rbac_rule_validation.action(service="neutron",
+ rules=["get_trunk",
+ "delete_trunk"],
+ expected_error_codes=[404, 403])
+ def test_delete_trunk(self):
+ """Delete trunk.
+
+ RBAC test for the neutron "delete_trunk" policy
+ """
+ trunk = self.create_trunk(self.port_id)
+
+ with self.rbac_utils.override_role(self):
+ self.ntp_client.delete_trunk(trunk['trunk']['id'])
diff --git a/patrole_tempest_plugin/tests/api/volume/rbac_base.py b/patrole_tempest_plugin/tests/api/volume/rbac_base.py
index bac173e..14b3151 100644
--- a/patrole_tempest_plugin/tests/api/volume/rbac_base.py
+++ b/patrole_tempest_plugin/tests/api/volume/rbac_base.py
@@ -13,14 +13,11 @@
from tempest.api.volume import base as vol_base
from tempest.common import waiters
-from tempest import config
from tempest.lib.common.utils import data_utils
from tempest.lib.common.utils import test_utils
from patrole_tempest_plugin import rbac_utils
-CONF = config.CONF
-
class BaseVolumeRbacTest(rbac_utils.RbacUtilsMixin,
vol_base.BaseVolumeTest):
@@ -32,11 +29,6 @@
_api_version = 3
@classmethod
- def skip_checks(cls):
- super(BaseVolumeRbacTest, cls).skip_checks()
- cls.skip_rbac_checks()
-
- @classmethod
def setup_clients(cls):
super(BaseVolumeRbacTest, cls).setup_clients()
cls.setup_rbac_utils()
diff --git a/patrole_tempest_plugin/tests/api/volume/test_encryption_types_rbac.py b/patrole_tempest_plugin/tests/api/volume/test_encryption_types_rbac.py
index f10e41b..2ee80eb 100644
--- a/patrole_tempest_plugin/tests/api/volume/test_encryption_types_rbac.py
+++ b/patrole_tempest_plugin/tests/api/volume/test_encryption_types_rbac.py
@@ -13,12 +13,36 @@
# License for the specific language governing permissions and limitations
# under the License.
+import functools
+
from tempest.common import utils
+from tempest import config
from tempest.lib import decorators
from patrole_tempest_plugin import rbac_rule_validation
from patrole_tempest_plugin.tests.api.volume import rbac_base
+CONF = config.CONF
+
+
+def _get_volume_type_encryption_policy(action):
+ feature_flag = CONF.policy_feature_enabled.added_cinder_policies_stein
+
+ if feature_flag:
+ return "volume_extension:volume_type_encryption:%s" % action
+
+ return "volume_extension:volume_type_encryption"
+
+
+_CREATE_VOLUME_TYPE_ENCRYPTION = functools.partial(
+ _get_volume_type_encryption_policy, "create")
+_SHOW_VOLUME_TYPE_ENCRYPTION = functools.partial(
+ _get_volume_type_encryption_policy, "get")
+_UPDATE_VOLUME_TYPE_ENCRYPTION = functools.partial(
+ _get_volume_type_encryption_policy, "update")
+_DELETE_VOLUME_TYPE_ENCRYPTION = functools.partial(
+ _get_volume_type_encryption_policy, "delete")
+
class EncryptionTypesV3RbacTest(rbac_base.BaseVolumeRbacTest):
@@ -45,7 +69,7 @@
@decorators.idempotent_id('ffd94ce5-c24b-4b6c-84c9-c5aad8c3010c')
@rbac_rule_validation.action(
service="cinder",
- rule="volume_extension:volume_type_encryption")
+ rule=_CREATE_VOLUME_TYPE_ENCRYPTION)
def test_create_volume_type_encryption(self):
vol_type_id = self.create_volume_type()['id']
with self.rbac_utils.override_role(self):
@@ -57,7 +81,7 @@
@decorators.idempotent_id('6599e72e-acef-4c0d-a9b2-463fca30d1da')
@rbac_rule_validation.action(
service="cinder",
- rule="volume_extension:volume_type_encryption")
+ rule=_DELETE_VOLUME_TYPE_ENCRYPTION)
def test_delete_volume_type_encryption(self):
vol_type_id = self._create_volume_type_encryption()
with self.rbac_utils.override_role(self):
@@ -66,7 +90,7 @@
@decorators.idempotent_id('42da9fec-32fd-4dca-9242-8a53b2fed25a')
@rbac_rule_validation.action(
service="cinder",
- rule="volume_extension:volume_type_encryption")
+ rule=_UPDATE_VOLUME_TYPE_ENCRYPTION)
def test_update_volume_type_encryption(self):
vol_type_id = self._create_volume_type_encryption()
with self.rbac_utils.override_role(self):
@@ -77,7 +101,7 @@
@decorators.idempotent_id('1381a3dc-248f-4282-b231-c9399018c804')
@rbac_rule_validation.action(
service="cinder",
- rule="volume_extension:volume_type_encryption")
+ rule=_SHOW_VOLUME_TYPE_ENCRYPTION)
def test_show_volume_type_encryption(self):
vol_type_id = self._create_volume_type_encryption()
with self.rbac_utils.override_role(self):
@@ -86,7 +110,7 @@
@decorators.idempotent_id('d4ed3cf8-52b2-4fa2-910d-e405361f0881')
@rbac_rule_validation.action(
service="cinder",
- rule="volume_extension:volume_type_encryption")
+ rule=_SHOW_VOLUME_TYPE_ENCRYPTION)
def test_show_encryption_specs_item(self):
vol_type_id = self._create_volume_type_encryption()
with self.rbac_utils.override_role(self):
diff --git a/patrole_tempest_plugin/tests/unit/fixtures.py b/patrole_tempest_plugin/tests/unit/fixtures.py
index 1c47985..4552224 100644
--- a/patrole_tempest_plugin/tests/unit/fixtures.py
+++ b/patrole_tempest_plugin/tests/unit/fixtures.py
@@ -16,6 +16,7 @@
"""Fixtures for Patrole tests."""
from __future__ import absolute_import
+from contextlib import contextmanager
import fixtures
import mock
import time
@@ -117,6 +118,17 @@
new_role = 'member' if role_toggle else 'admin'
self.set_roles(['admin', 'member'], [new_role])
+ @contextmanager
+ def real_override_role(self, test_obj):
+ """Actual call to ``override_role``.
+
+ Useful for ensuring all the necessary mocks are performed before
+ the method in question is called.
+ """
+ _rbac_utils = rbac_utils.RbacUtils(test_obj)
+ with _rbac_utils.override_role(test_obj):
+ yield
+
def set_roles(self, roles, roles_on_project=None):
"""Set the list of available roles in the system.
diff --git a/patrole_tempest_plugin/tests/unit/resources/custom_rbac_policy.yaml b/patrole_tempest_plugin/tests/unit/resources/custom_rbac_policy.yaml
new file mode 100644
index 0000000..444bd2e
--- /dev/null
+++ b/patrole_tempest_plugin/tests/unit/resources/custom_rbac_policy.yaml
@@ -0,0 +1,13 @@
+---
+even_rule: role:two or role:four or role:six or role:eight
+odd_rule: role:one or role:three or role:five or role:seven or role:nine
+zero_rule: role:zero
+prime_rule: role:one or role:two or role:three or role:five or role:seven
+all_rule: ''
+
+policy_action_1: rule:even_rule
+policy_action_2: rule:odd_rule
+policy_action_3: rule:zero_rule
+policy_action_4: rule:prime_rule
+policy_action_5: rule:all_rule
+policy_action_6: role:eight
diff --git a/patrole_tempest_plugin/tests/unit/test_policy_authority.py b/patrole_tempest_plugin/tests/unit/test_policy_authority.py
index d396a29..624c0c5 100644
--- a/patrole_tempest_plugin/tests/unit/test_policy_authority.py
+++ b/patrole_tempest_plugin/tests/unit/test_policy_authority.py
@@ -13,7 +13,6 @@
# License for the specific language governing permissions and limitations
# under the License.
-import json
import mock
import os
@@ -61,11 +60,14 @@
self.tenant_policy_file = os.path.join(current_directory,
'resources',
'tenant_rbac_policy.json')
- self.conf_policy_path = os.path.join(
+ self.conf_policy_path_json = os.path.join(
current_directory, 'resources', '%s.json')
+ self.conf_policy_path_yaml = os.path.join(
+ current_directory, 'resources', '%s.yaml')
+
self.useFixture(fixtures.ConfPatcher(
- custom_policy_files=[self.conf_policy_path], group='patrole'))
+ custom_policy_files=[self.conf_policy_path_json], group='patrole'))
self.useFixture(fixtures.ConfPatcher(
api_v3=True, api_v2=False, group='identity-feature-enabled'))
@@ -74,13 +76,18 @@
if attr in dir(policy_authority.PolicyAuthority):
delattr(policy_authority.PolicyAuthority, attr)
- def _get_fake_policy_rule(self, name, rule):
- fake_rule = mock.Mock(check=rule, __name__='foo')
- fake_rule.name = name
- return fake_rule
+ @staticmethod
+ def _get_fake_policies(rules):
+ fake_rules = []
+ rules = policy_authority.policy.Rules.from_dict(rules)
+ for name, check in rules.items():
+ fake_rule = mock.Mock(check=check, __name__='foo')
+ fake_rule.name = name
+ fake_rules.append(fake_rule)
+ return fake_rules
@mock.patch.object(policy_authority, 'LOG', autospec=True)
- def test_custom_policy(self, m_log):
+ def _test_custom_policy(self, *args):
default_roles = ['zero', 'one', 'two', 'three', 'four',
'five', 'six', 'seven', 'eight', 'nine']
@@ -105,6 +112,16 @@
for role in set(default_roles) - set(role_list):
self.assertFalse(authority.allowed(rule, role))
+ def test_custom_policy_json(self):
+ # The CONF.patrole.custom_policy_files has a path to JSON file by
+ # default, so we don't need to use ConfPatcher here.
+ self._test_custom_policy()
+
+ def test_custom_policy_yaml(self):
+ self.useFixture(fixtures.ConfPatcher(
+ custom_policy_files=[self.conf_policy_path_yaml], group='patrole'))
+ self._test_custom_policy()
+
def test_admin_policy_file_with_admin_role(self):
test_tenant_id = mock.sentinel.tenant_id
test_user_id = mock.sentinel.user_id
@@ -270,9 +287,9 @@
fake_rule = 'fake_rule'
expected_message = (
- "Policy action \"{0}\" not found in policy file: {1} or among "
- "registered policy in code defaults for service.").format(
- fake_rule, self.custom_policy_file)
+ 'Policy action "{0}" not found in policy files: {1} or among '
+ 'registered policy in code defaults for {2} service.').format(
+ fake_rule, [self.custom_policy_file], "custom_rbac_policy")
e = self.assertRaises(rbac_exceptions.RbacParsingException,
authority.allowed, fake_rule, None)
@@ -292,9 +309,10 @@
mock.sentinel.error)})
expected_message = (
- "Policy action \"{0}\" not found in policy file: {1} or among "
- "registered policy in code defaults for service.").format(
- mock.sentinel.rule, self.custom_policy_file)
+ 'Policy action "{0}" not found in policy files: {1} or among '
+ 'registered policy in code defaults for {2} service.').format(
+ mock.sentinel.rule, [self.custom_policy_file],
+ "custom_rbac_policy")
e = self.assertRaises(rbac_exceptions.RbacParsingException,
authority.allowed, mock.sentinel.rule, None)
@@ -302,18 +320,15 @@
m_log.debug.assert_called_once_with(expected_message)
@mock.patch.object(policy_authority, 'stevedore', autospec=True)
- def test_get_policy_data_from_file_and_from_code(self, mock_stevedore):
- fake_policy_rules = [
- self._get_fake_policy_rule('code_policy_action_1',
- 'rule:code_rule_1'),
- self._get_fake_policy_rule('code_policy_action_2',
- 'rule:code_rule_2'),
- self._get_fake_policy_rule('code_policy_action_3',
- 'rule:code_rule_3'),
- ]
+ def test_get_rules_from_file_and_from_code(self, mock_stevedore):
+ fake_policy_rules = self._get_fake_policies({
+ 'code_policy_action_1': 'rule:code_rule_1',
+ 'code_policy_action_2': 'rule:code_rule_2',
+ 'code_policy_action_3': 'rule:code_rule_3',
+ })
mock_manager = mock.Mock(obj=fake_policy_rules, __name__='foo')
- mock_manager.configure_mock(name='fake_service')
+ mock_manager.configure_mock(name='tenant_rbac_policy')
mock_stevedore.named.NamedExtensionManager.return_value = [
mock_manager
]
@@ -323,10 +338,10 @@
authority = policy_authority.PolicyAuthority(
test_tenant_id, test_user_id, "tenant_rbac_policy")
- policy_data = authority._get_policy_data('fake_service')
- self.assertIsInstance(policy_data, str)
+ rules = authority.get_rules()
+ self.assertIsInstance(rules, policy_authority.policy.Rules)
- actual_policy_data = json.loads(policy_data)
+ actual_policy_data = {k: str(v) for k, v in rules.items()}
expected_policy_data = {
"code_policy_action_1": "rule:code_rule_1",
"code_policy_action_2": "rule:code_rule_2",
@@ -335,26 +350,25 @@
"rule2": "tenant_id:%(tenant_id)s",
"rule3": "project_id:%(project_id)s",
"rule4": "user_id:%(user_id)s",
- "admin_tenant_rule": "role:admin and tenant_id:%(tenant_id)s",
- "admin_user_rule": "role:admin and user_id:%(user_id)s"
+ "admin_tenant_rule": "(role:admin and tenant_id:%(tenant_id)s)",
+ "admin_user_rule": "(role:admin and user_id:%(user_id)s)"
}
self.assertEqual(expected_policy_data, actual_policy_data)
@mock.patch.object(policy_authority, 'stevedore', autospec=True)
- def test_get_policy_data_from_file_and_from_code_with_overwrite(
+ def test_get_rules_from_file_and_from_code_with_overwrite(
self, mock_stevedore):
# The custom policy file should overwrite default rules rule1 and rule2
# that are defined in code.
- fake_policy_rules = [
- self._get_fake_policy_rule('rule1', 'rule:code_rule_1'),
- self._get_fake_policy_rule('rule2', 'rule:code_rule_2'),
- self._get_fake_policy_rule('code_policy_action_3',
- 'rule:code_rule_3'),
- ]
+ fake_policy_rules = self._get_fake_policies({
+ 'rule1': 'rule:code_rule_1',
+ 'rule2': 'rule:code_rule_2',
+ 'code_policy_action_3': 'rule:code_rule_3',
+ })
mock_manager = mock.Mock(obj=fake_policy_rules, __name__='foo')
- mock_manager.configure_mock(name='fake_service')
+ mock_manager.configure_mock(name='tenant_rbac_policy')
mock_stevedore.named.NamedExtensionManager.return_value = [
mock_manager
]
@@ -364,81 +378,51 @@
authority = policy_authority.PolicyAuthority(
test_tenant_id, test_user_id, 'tenant_rbac_policy')
- policy_data = authority._get_policy_data('fake_service')
- self.assertIsInstance(policy_data, str)
+ rules = authority.get_rules()
+ self.assertIsInstance(rules, policy_authority.policy.Rules)
- actual_policy_data = json.loads(policy_data)
+ actual_policy_data = {k: str(v) for k, v in rules.items()}
expected_policy_data = {
"code_policy_action_3": "rule:code_rule_3",
"rule1": "tenant_id:%(network:tenant_id)s",
"rule2": "tenant_id:%(tenant_id)s",
"rule3": "project_id:%(project_id)s",
"rule4": "user_id:%(user_id)s",
- "admin_tenant_rule": "role:admin and tenant_id:%(tenant_id)s",
- "admin_user_rule": "role:admin and user_id:%(user_id)s"
+ "admin_tenant_rule": "(role:admin and tenant_id:%(tenant_id)s)",
+ "admin_user_rule": "(role:admin and user_id:%(user_id)s)"
}
self.assertEqual(expected_policy_data, actual_policy_data)
@mock.patch.object(policy_authority, 'stevedore', autospec=True)
- def test_get_policy_data_cannot_find_policy(self, mock_stevedore):
+ def test_get_rules_cannot_find_policy(self, mock_stevedore):
mock_stevedore.named.NamedExtensionManager.return_value = None
e = self.assertRaises(rbac_exceptions.RbacParsingException,
policy_authority.PolicyAuthority,
None, None, 'test_service')
expected_error = (
- 'Policy file for {0} service was not found among the registered '
+ 'Policy files for {0} service were not found among the registered '
'in-code policies or in any of the possible policy files: {1}.'
.format('test_service',
[CONF.patrole.custom_policy_files[0] % 'test_service']))
self.assertIn(expected_error, str(e))
- @mock.patch.object(policy_authority, 'json', autospec=True)
+ @mock.patch.object(policy_authority.policy, 'parse_file_contents',
+ autospec=True)
@mock.patch.object(policy_authority, 'stevedore', autospec=True)
- def test_get_policy_data_without_valid_policy(self, mock_stevedore,
- mock_json):
- test_policy_action = mock.Mock(check='rule:bar', __name__='foo')
- test_policy_action.configure_mock(name='foo')
-
- test_policy = mock.Mock(obj=[test_policy_action], __name__='foo')
- test_policy.configure_mock(name='test_service')
-
- mock_stevedore.named.NamedExtensionManager\
- .return_value = [test_policy]
-
- mock_json.dumps.side_effect = ValueError
-
- e = self.assertRaises(rbac_exceptions.RbacParsingException,
- policy_authority.PolicyAuthority,
- None, None, 'test_service')
-
- expected_error = "Policy file for {0} service is invalid."\
- .format("test_service")
- self.assertIn(expected_error, str(e))
-
- mock_stevedore.named.NamedExtensionManager.assert_called_once_with(
- 'oslo.policy.policies',
- names=['test_service'],
- on_load_failure_callback=None,
- invoke_on_load=True,
- warn_on_missing_entrypoint=False)
-
- @mock.patch.object(policy_authority, 'json', autospec=True)
- @mock.patch.object(policy_authority, 'stevedore', autospec=True)
- def test_get_policy_data_from_file_not_json(self, mock_stevedore,
- mock_json):
+ def test_get_rules_without_valid_policy(self, mock_stevedore,
+ mock_parse_file_contents):
mock_stevedore.named.NamedExtensionManager.return_value = None
- mock_json.loads.side_effect = ValueError
+ mock_parse_file_contents.side_effect = ValueError
e = self.assertRaises(rbac_exceptions.RbacParsingException,
policy_authority.PolicyAuthority,
None, None, 'tenant_rbac_policy')
expected_error = (
- 'Policy file for {0} service was not found among the registered '
- 'in-code policies or in any of the possible policy files: {1}.'
- .format('tenant_rbac_policy', [CONF.patrole.custom_policy_files[0]
- % 'tenant_rbac_policy']))
+ 'Policy files for {0} service were not found among the registered '
+ 'in-code policies or in any of the possible policy files:'
+ .format('tenant_rbac_policy'))
self.assertIn(expected_error, str(e))
def test_discover_policy_files(self):
@@ -450,56 +434,62 @@
dir(policy_authority.PolicyAuthority))
self.assertIn('policy_files', dir(policy_parser))
self.assertIn('tenant_rbac_policy', policy_parser.policy_files)
- self.assertEqual(self.conf_policy_path % 'tenant_rbac_policy',
+ self.assertEqual([self.conf_policy_path_json % 'tenant_rbac_policy'],
policy_parser.policy_files['tenant_rbac_policy'])
@mock.patch.object(policy_authority, 'policy', autospec=True)
- @mock.patch.object(policy_authority.PolicyAuthority, '_get_policy_data',
+ @mock.patch.object(policy_authority.PolicyAuthority, 'get_rules',
autospec=True)
@mock.patch.object(policy_authority, 'clients', autospec=True)
@mock.patch.object(policy_authority, 'os', autospec=True)
- def test_discover_policy_files_with_many_invalid_one_valid(self, m_os,
- m_creds, *args):
+ @mock.patch.object(policy_authority, 'glob', autospec=True)
+ def test_discover_policy_files_with_many_invalid_one_valid(self, m_glob,
+ m_os, m_creds,
+ *args):
+ service = 'test_service'
+ custom_policy_files = ['foo/%s', 'bar/%s', 'baz/%s']
+ m_glob.iglob.side_effect = [iter([path % service])
+ for path in custom_policy_files]
# Only the 3rd path is valid.
- m_os.path.isfile.side_effect = [False, False, True, False]
+ m_os.path.isfile.side_effect = [False, False, True]
# Ensure the outer for loop runs only once in `discover_policy_files`.
m_creds.Manager().identity_services_v3_client.\
list_services.return_value = {
- 'services': [{'name': 'test_service'}]}
+ 'services': [{'name': service}]}
# The expected policy will be 'baz/test_service'.
self.useFixture(fixtures.ConfPatcher(
- custom_policy_files=['foo/%s', 'bar/%s', 'baz/%s'],
+ custom_policy_files=custom_policy_files,
group='patrole'))
policy_parser = policy_authority.PolicyAuthority(
- None, None, 'test_service')
+ None, None, service)
# Ensure that "policy_files" is set at class and instance levels.
- self.assertIn('policy_files',
- dir(policy_authority.PolicyAuthority))
- self.assertIn('policy_files', dir(policy_parser))
- self.assertIn('test_service', policy_parser.policy_files)
- self.assertEqual('baz/test_service',
- policy_parser.policy_files['test_service'])
+ self.assertTrue(hasattr(policy_authority.PolicyAuthority,
+ 'policy_files'))
+ self.assertTrue(hasattr(policy_parser, 'policy_files'))
+ self.assertEqual(['baz/%s' % service],
+ policy_parser.policy_files[service])
def test_discover_policy_files_with_no_valid_files(self):
expected_error = (
- 'Policy file for {0} service was not found among the registered '
+ 'Policy files for {0} service were not found among the registered '
'in-code policies or in any of the possible policy files: {1}.'
- .format('test_service', [self.conf_policy_path % 'test_service']))
+ .format('test_service',
+ [self.conf_policy_path_json % 'test_service']))
e = self.assertRaises(rbac_exceptions.RbacParsingException,
policy_authority.PolicyAuthority,
None, None, 'test_service')
self.assertIn(expected_error, str(e))
- self.assertIn('policy_files',
- dir(policy_authority.PolicyAuthority))
- self.assertNotIn(
- 'test_service',
- policy_authority.PolicyAuthority.policy_files.keys())
+ self.assertTrue(hasattr(policy_authority.PolicyAuthority,
+ 'policy_files'))
+ self.assertEqual(
+ [],
+ policy_authority.PolicyAuthority.policy_files['test_service'])
def _test_validate_service(self, v2_services, v3_services,
expected_failure=False, expected_services=None):
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 1bf5510..1772047 100644
--- a/patrole_tempest_plugin/tests/unit/test_rbac_rule_validation.py
+++ b/patrole_tempest_plugin/tests/unit/test_rbac_rule_validation.py
@@ -12,9 +12,13 @@
# License for the specific language governing permissions and limitations
# under the License.
+from __future__ import absolute_import
+
+import functools
import mock
from oslo_config import cfg
+import fixtures
from tempest.lib import exceptions
from tempest import manager
from tempest import test
@@ -23,7 +27,7 @@
from patrole_tempest_plugin import rbac_exceptions
from patrole_tempest_plugin import rbac_rule_validation as rbac_rv
from patrole_tempest_plugin import rbac_utils
-from patrole_tempest_plugin.tests.unit import fixtures
+from patrole_tempest_plugin.tests.unit import fixtures as patrole_fixtures
CONF = cfg.CONF
@@ -43,10 +47,12 @@
setattr(self.mock_test_args.os_primary, 'credentials', mock_creds)
self.useFixture(
- fixtures.ConfPatcher(rbac_test_role='Member', group='patrole'))
+ patrole_fixtures.ConfPatcher(rbac_test_role='Member',
+ group='patrole'))
# Disable patrole log for unit tests.
self.useFixture(
- fixtures.ConfPatcher(enable_reporting=False, group='patrole_log'))
+ patrole_fixtures.ConfPatcher(enable_reporting=False,
+ group='patrole_log'))
class RBACRuleValidationTest(BaseRBACRuleValidationTest):
@@ -54,6 +60,12 @@
``rbac_rule_validation`` decorator.
"""
+ def setUp(self):
+ super(RBACRuleValidationTest, self).setUp()
+ # This behavior is tested in separate test class below.
+ self.useFixture(fixtures.MockPatchObject(
+ rbac_rv, '_validate_override_role_called'))
+
@mock.patch.object(rbac_rv, 'LOG', autospec=True)
@mock.patch.object(rbac_rv, 'policy_authority', autospec=True)
def test_rule_validation_have_permission_no_exc(self, mock_authority,
@@ -69,7 +81,6 @@
pass
test_policy(self.mock_test_args)
- mock_log.warning.assert_not_called()
mock_log.error.assert_not_called()
@mock.patch.object(rbac_rv, 'LOG', autospec=True)
@@ -88,7 +99,6 @@
raise exceptions.Forbidden()
test_policy(self.mock_test_args)
- mock_log.warning.assert_not_called()
mock_log.error.assert_not_called()
@mock.patch.object(rbac_rv, 'LOG', autospec=True)
@@ -119,7 +129,8 @@
@mock.patch.object(rbac_rv, 'policy_authority', autospec=True)
def test_rule_validation_rbac_malformed_response_positive(
self, mock_authority, mock_log):
- """Test RbacMalformedResponse error is thrown without permission passes.
+ """Test RbacMalformedResponse error is thrown without permission
+ passes.
Positive test case: if RbacMalformedResponse is thrown and the user is
not allowed to perform the action, then this is a success.
@@ -132,7 +143,6 @@
raise rbac_exceptions.RbacMalformedResponse()
mock_log.error.assert_not_called()
- mock_log.warning.assert_not_called()
@mock.patch.object(rbac_rv, 'LOG', autospec=True)
@mock.patch.object(rbac_rv, 'policy_authority', autospec=True)
@@ -160,7 +170,8 @@
@mock.patch.object(rbac_rv, 'policy_authority', autospec=True)
def test_rule_validation_rbac_conflicting_policies_positive(
self, mock_authority, mock_log):
- """Test RbacConflictingPolicies error is thrown without permission passes.
+ """Test RbacConflictingPolicies error is thrown without permission
+ passes.
Positive test case: if RbacConflictingPolicies is thrown and the user
is not allowed to perform the action, then this is a success.
@@ -173,7 +184,6 @@
raise rbac_exceptions.RbacConflictingPolicies()
mock_log.error.assert_not_called()
- mock_log.warning.assert_not_called()
@mock.patch.object(rbac_rv, 'LOG', autospec=True)
@mock.patch.object(rbac_rv, 'policy_authority', autospec=True)
@@ -269,7 +279,7 @@
mock_log.warning.assert_called_with(
"NotFound exception was caught for test %s. Expected policies "
"which may have caused the error: %s. The service %s throws a "
- "404 instead of a 403, which is irregular.",
+ "404 instead of a 403, which is irregular",
test_policy.__name__,
', '.join(policy_names),
mock.sentinel.service)
@@ -334,7 +344,7 @@
expected_irregular_msg = (
"NotFound exception was caught for test %s. Expected policies "
"which may have caused the error: %s. The service %s throws a "
- "404 instead of a 403, which is irregular.")
+ "404 instead of a 403, which is irregular")
actual_exception, actual_irregular_msg = \
rbac_rv._get_exception_type(404)
@@ -385,6 +395,12 @@
Patrole RBAC validation work flows.
"""
+ def setUp(self):
+ super(RBACRuleValidationLoggingTest, self).setUp()
+ # This behavior is tested in separate test class below.
+ self.useFixture(fixtures.MockPatchObject(
+ rbac_rv, '_validate_override_role_called'))
+
@mock.patch.object(rbac_rv, 'RBACLOG', autospec=True)
@mock.patch.object(rbac_rv, 'policy_authority', autospec=True)
def test_rbac_report_logging_disabled(self, mock_authority, mock_rbaclog):
@@ -392,7 +408,8 @@
is False
"""
self.useFixture(
- fixtures.ConfPatcher(enable_reporting=False, group='patrole_log'))
+ patrole_fixtures.ConfPatcher(enable_reporting=False,
+ group='patrole_log'))
mock_authority.PolicyAuthority.return_value.allowed.return_value = True
@@ -410,7 +427,8 @@
True
"""
self.useFixture(
- fixtures.ConfPatcher(enable_reporting=True, group='patrole_log'))
+ patrole_fixtures.ConfPatcher(enable_reporting=True,
+ group='patrole_log'))
mock_authority.PolicyAuthority.return_value.allowed.return_value = True
policy_names = ['foo:bar', 'baz:qux']
@@ -429,9 +447,75 @@
"Allowed",
"Allowed")
+ @mock.patch.object(rbac_rv, 'LOG', autospec=True)
+ @mock.patch.object(rbac_rv, 'policy_authority', autospec=True)
+ def test_rule_validation_with_callable_rule(self, mock_authority,
+ mock_log):
+ """Test that a callable as the rule is evaluated correctly."""
+ mock_authority.PolicyAuthority.return_value.allowed.return_value = True
+
+ @rbac_rv.action(mock.sentinel.service,
+ rule=lambda: mock.sentinel.action)
+ def test_policy(*args):
+ pass
+
+ test_policy(self.mock_test_args)
+
+ policy_authority = mock_authority.PolicyAuthority.return_value
+ policy_authority.allowed.assert_called_with(
+ mock.sentinel.action,
+ CONF.patrole.rbac_test_role)
+
+ mock_log.error.assert_not_called()
+
+ @mock.patch.object(rbac_rv, 'LOG', autospec=True)
+ @mock.patch.object(rbac_rv, 'policy_authority', autospec=True)
+ def test_rule_validation_with_conditional_callable_rule(
+ self, mock_authority, mock_log):
+ """Test that a complex callable with conditional logic as the rule is
+ evaluated correctly.
+ """
+ mock_authority.PolicyAuthority.return_value.allowed.return_value = True
+
+ def partial_func(x):
+ return "foo" if x == "bar" else "qux"
+ foo_callable = functools.partial(partial_func, "bar")
+ bar_callable = functools.partial(partial_func, "baz")
+
+ @rbac_rv.action(mock.sentinel.service,
+ rule=foo_callable)
+ def test_foo_policy(*args):
+ pass
+
+ @rbac_rv.action(mock.sentinel.service,
+ rule=bar_callable)
+ def test_bar_policy(*args):
+ pass
+
+ test_foo_policy(self.mock_test_args)
+ policy_authority = mock_authority.PolicyAuthority.return_value
+ policy_authority.allowed.assert_called_with(
+ "foo",
+ CONF.patrole.rbac_test_role)
+ policy_authority.allowed.reset_mock()
+
+ test_bar_policy(self.mock_test_args)
+ policy_authority = mock_authority.PolicyAuthority.return_value
+ policy_authority.allowed.assert_called_with(
+ "qux",
+ CONF.patrole.rbac_test_role)
+
+ mock_log.error.assert_not_called()
+
class RBACRuleValidationNegativeTest(BaseRBACRuleValidationTest):
+ def setUp(self):
+ super(RBACRuleValidationNegativeTest, self).setUp()
+ # This behavior is tested in separate test class below.
+ self.useFixture(fixtures.MockPatchObject(
+ rbac_rv, '_validate_override_role_called'))
+
@mock.patch.object(rbac_rv, 'policy_authority', autospec=True)
def test_rule_validation_invalid_service_raises_exc(self, mock_authority):
"""Test that invalid service raises the appropriate exception."""
@@ -451,6 +535,12 @@
``rbac_rule_validation`` decorator.
"""
+ def setUp(self):
+ super(RBACRuleValidationTestMultiPolicy, self).setUp()
+ # This behavior is tested in separate test class below.
+ self.useFixture(fixtures.MockPatchObject(
+ rbac_rv, '_validate_override_role_called'))
+
def _assert_policy_authority_called_with(self, rules, mock_authority):
m_authority = mock_authority.PolicyAuthority.return_value
m_authority.allowed.assert_has_calls([
@@ -708,3 +798,189 @@
# When expected_error_codes is provided rules must be as well.
self.assertRaisesRegex(ValueError, error_re, _do_test,
None, None, None, [404])
+
+
+class RBACOverrideRoleValidationTest(BaseRBACRuleValidationTest):
+ """Class for validating that untimely exceptions (outside
+ ``override_role`` is called) result in test failures.
+
+ This regression tests false positives caused by test exceptions matching
+ the expected exception before or after the ``override_role`` context is
+ called. Also tests case where ``override_role`` is never called which is
+ an invalid Patrole test.
+
+ """
+
+ def setUp(self):
+ super(RBACOverrideRoleValidationTest, self).setUp()
+
+ # Mixin automatically initializes __override_role_called to False.
+ class FakeRbacTest(rbac_utils.RbacUtilsMixin, test.BaseTestCase):
+ def runTest(self):
+ pass
+
+ # Stub out problematic function calls.
+ FakeRbacTest.os_primary = mock.Mock(spec=manager.Manager)
+ FakeRbacTest.rbac_utils = self.useFixture(
+ patrole_fixtures.RbacUtilsFixture())
+ mock_creds = mock.Mock(user_id=mock.sentinel.user_id,
+ project_id=mock.sentinel.project_id)
+ setattr(FakeRbacTest.os_primary, 'credentials', mock_creds)
+ setattr(FakeRbacTest.os_primary, 'auth_provider', mock.Mock())
+
+ self.parent_class = FakeRbacTest
+
+ @mock.patch.object(rbac_rv, 'policy_authority', autospec=True)
+ def test_rule_validation_override_role_called_inside_ctx(self,
+ mock_authority):
+ """Test success case when the expected exception is raised within the
+ override_role context.
+ """
+ mock_authority.PolicyAuthority.return_value.allowed.return_value =\
+ False
+
+ class ChildRbacTest(self.parent_class):
+
+ @rbac_rv.action(mock.sentinel.service, rules=["fake:rule"],
+ expected_error_codes=[404])
+ def test_called(self_):
+ with self_.rbac_utils.real_override_role(self_):
+ raise exceptions.NotFound()
+
+ child_test = ChildRbacTest()
+ child_test.test_called()
+
+ @mock.patch.object(rbac_rv, 'policy_authority', autospec=True)
+ def test_rule_validation_override_role_patrole_exception_ignored(
+ self, mock_authority):
+ """Test success case where Patrole exception is raised (which is
+ valid in case of e.g. RbacMalformedException) after override_role
+ passes.
+ """
+ mock_authority.PolicyAuthority.return_value.allowed.return_value =\
+ True
+
+ class ChildRbacTest(self.parent_class):
+
+ @rbac_rv.action(mock.sentinel.service, rules=["fake:rule"],
+ expected_error_codes=[404])
+ def test_called(self_):
+ with self_.rbac_utils.real_override_role(self_):
+ pass
+ # Instances of BasePatroleException don't count as they are
+ # part of the validation work flow.
+ raise rbac_exceptions.BasePatroleException()
+
+ child_test = ChildRbacTest()
+ self.assertRaises(rbac_exceptions.BasePatroleException,
+ child_test.test_called)
+
+ @mock.patch.object(rbac_rv, 'policy_authority', autospec=True)
+ def test_rule_validation_override_role_called_before_ctx(self,
+ mock_authority):
+ """Test failure case when an exception that happens before
+ ``override_role`` context, even if it is the expected exception,
+ raises ``RbacOverrideRoleException``.
+ """
+ mock_authority.PolicyAuthority.return_value.allowed.return_value =\
+ False
+
+ # This behavior should work for supported (NotFound/Forbidden) and
+ # miscellaneous exceptions alike.
+ for exception_type in (exceptions.NotFound,
+ Exception):
+ class ChildRbacTest(self.parent_class):
+
+ @rbac_rv.action(mock.sentinel.service, rules=["fake:rule"],
+ expected_error_codes=[404])
+ def test_called_before(self_):
+ raise exception_type()
+
+ child_test = ChildRbacTest()
+ test_re = ".*before.*"
+ self.assertRaisesRegex(rbac_exceptions.RbacOverrideRoleException,
+ test_re, child_test.test_called_before)
+
+ @mock.patch.object(rbac_rv, 'policy_authority', autospec=True)
+ def test_rule_validation_override_role_called_after_ctx(self,
+ mock_authority):
+ """Test failure case when an exception that happens before
+ ``override_role`` context, even if it is the expected exception,
+ raises ``RbacOverrideRoleException``.
+ """
+ mock_authority.PolicyAuthority.return_value.allowed.return_value =\
+ False
+
+ # This behavior should work for supported (NotFound/Forbidden) and
+ # miscellaneous exceptions alike.
+ for exception_type in (exceptions.NotFound,
+ Exception):
+ class ChildRbacTest(self.parent_class):
+
+ @rbac_rv.action(mock.sentinel.service, rules=["fake:rule"],
+ expected_error_codes=[404])
+ def test_called_after(self_):
+ with self_.rbac_utils.real_override_role(self_):
+ pass
+ # Simulates a test tearDown failure or some such.
+ raise exception_type()
+
+ child_test = ChildRbacTest()
+ test_re = ".*after.*"
+ self.assertRaisesRegex(rbac_exceptions.RbacOverrideRoleException,
+ test_re, child_test.test_called_after)
+
+ @mock.patch.object(rbac_rv, 'policy_authority', autospec=True)
+ def test_rule_validation_override_role_never_called(self, mock_authority):
+ """Test failure case where override_role is **never** called."""
+ mock_authority.PolicyAuthority.return_value.allowed.return_value =\
+ False
+
+ class ChildRbacTest(self.parent_class):
+
+ @rbac_rv.action(mock.sentinel.service, rules=["fake:rule"],
+ expected_error_codes=[404])
+ def test_never_called(self_):
+ pass
+
+ child_test = ChildRbacTest()
+ test_re = ".*missing required `override_role` call.*"
+ self.assertRaisesRegex(rbac_exceptions.RbacOverrideRoleException,
+ test_re, child_test.test_never_called)
+
+ @mock.patch.object(rbac_rv, 'policy_authority', autospec=True)
+ def test_rule_validation_override_role_sequential_test_calls(
+ self, mock_authority):
+ """Test success/failure scenarios above across sequential test calls.
+ """
+ mock_authority.PolicyAuthority.return_value.allowed.return_value =\
+ False
+
+ class ChildRbacTest(self.parent_class):
+
+ @rbac_rv.action(mock.sentinel.service, rules=["fake:rule1"],
+ expected_error_codes=[404])
+ def test_called(self_):
+ with self_.rbac_utils.real_override_role(self_):
+ raise exceptions.NotFound()
+
+ @rbac_rv.action(mock.sentinel.service, rules=["fake:rule2"],
+ expected_error_codes=[404])
+ def test_called_before(self_):
+ raise exceptions.NotFound()
+
+ test_re = ".*before.*"
+
+ # Test case where override role is called in first test but *not* in
+ # second test.
+ child_test1 = ChildRbacTest()
+ child_test1.test_called()
+ self.assertRaisesRegex(rbac_exceptions.RbacOverrideRoleException,
+ test_re, child_test1.test_called_before)
+
+ # Test case where override role is *not* called in first test but is
+ # in second test.
+ child_test2 = ChildRbacTest()
+ self.assertRaisesRegex(rbac_exceptions.RbacOverrideRoleException,
+ test_re, child_test2.test_called_before)
+ child_test2.test_called()
diff --git a/patrole_tempest_plugin/tests/unit/test_rbac_utils.py b/patrole_tempest_plugin/tests/unit/test_rbac_utils.py
index c5264aa..5132079 100644
--- a/patrole_tempest_plugin/tests/unit/test_rbac_utils.py
+++ b/patrole_tempest_plugin/tests/unit/test_rbac_utils.py
@@ -208,11 +208,6 @@
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()
@@ -237,21 +232,3 @@
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=('Patrole testing not enabled so skipping %s.'
- % ChildRbacTest.__name__)):
- child_test.setUpClass()
diff --git a/releasenotes/notes/check-expected-errors-only-in-override-role-f7109a73f5ff70e2.yaml b/releasenotes/notes/check-expected-errors-only-in-override-role-f7109a73f5ff70e2.yaml
new file mode 100644
index 0000000..e0ac744
--- /dev/null
+++ b/releasenotes/notes/check-expected-errors-only-in-override-role-f7109a73f5ff70e2.yaml
@@ -0,0 +1,19 @@
+---
+features:
+ - |
+ Add new exception called ``RbacOverrideRoleException``. Used for
+ safeguarding against false positives that might occur when the expected
+ exception isn't raised inside the ``override_role`` context. Specifically,
+ when:
+
+ * ``override_role`` isn't called
+ * an exception is raised before ``override_role`` context
+ * an exception is raised after ``override_role`` context
+fixes:
+ - |
+ Previously, the ``rbac_rule_validation.action`` decorator could catch
+ expected exceptions with no regard to where the error happened. Such
+ behavior could cause false-positive results. To prevent this from
+ happening from now on, if an exception happens outside of the
+ ``override_role`` context, it will cause
+ ``rbac_exceptions.RbacOverrideRoleException`` to be raised.
diff --git a/releasenotes/notes/multiple-policy-files-9aa7f7583283739e.yaml b/releasenotes/notes/multiple-policy-files-9aa7f7583283739e.yaml
new file mode 100644
index 0000000..a3555e6
--- /dev/null
+++ b/releasenotes/notes/multiple-policy-files-9aa7f7583283739e.yaml
@@ -0,0 +1,17 @@
+---
+features:
+ - |
+ In order to implement the tests for plugins which do not maintain the
+ ``policy.json`` with full list of the policy rules and provide policy file
+ with only their own policy rules, the Patrole should be able to load and
+ merge multiple policy files for any of the services.
+
+ - Discovery all policy files for each of the services.
+ The updated ``discover_policy_files`` function picks all candidate paths
+ found out of the potential paths in the ``[patrole].custom_policy_files``
+ config option. Using ``glob.glob()`` function makes it possible to use
+ the patterns like '\*.json' to discover the policy files.
+
+ - Loading and merging a data from multiple policy files.
+ Patrole loads a data from each of the discovered policy files for a
+ service and merge the data from all files.
diff --git a/releasenotes/notes/remove-deprecated-api-extensions-policies-fca3d31c7f5f1f6c.yaml b/releasenotes/notes/remove-deprecated-api-extensions-policies-fca3d31c7f5f1f6c.yaml
new file mode 100644
index 0000000..925791f
--- /dev/null
+++ b/releasenotes/notes/remove-deprecated-api-extensions-policies-fca3d31c7f5f1f6c.yaml
@@ -0,0 +1,23 @@
+---
+features:
+ - |
+ A new policy feature flag called
+ ``[policy_feature_flag].removed_nova_policies_stein`` has been added to
+ Patrole's config to handle Nova API extension policies removed in Stein.
+
+ The policy feature flag is applied to tests that validate response bodies
+ for expected attributes previously returned for the following policies
+ that passed authorization:
+
+ - os_compute_api:os-config-drive
+ - os_compute_api:os-extended-availability-zone
+ - os_compute_api:os-extended-status
+ - os_compute_api:os-extended-volumes
+ - os_compute_api:os-keypairs
+ - os_compute_api:os-server-usage
+ - os_compute_api:os-flavor-rxtx
+ - os_compute_api:os-flavor-access (only from /flavors APIs)
+ - os_compute_api:image-size
+
+ Note that not all removed policies are included above because test coverage
+ is missing for them (like os_compute_api:os-security-groups).
diff --git a/releasenotes/notes/remove-deprecated-enable-rbac-config-option-a5e46ce1053b7dea.yaml b/releasenotes/notes/remove-deprecated-enable-rbac-config-option-a5e46ce1053b7dea.yaml
new file mode 100644
index 0000000..53b1710
--- /dev/null
+++ b/releasenotes/notes/remove-deprecated-enable-rbac-config-option-a5e46ce1053b7dea.yaml
@@ -0,0 +1,5 @@
+---
+upgrade:
+ - |
+ Remove deprecated ``[patrole].enable_rbac`` configuration option. To skip
+ Patrole tests going forward, use an appropriate regex.
diff --git a/releasenotes/notes/volume-type-encryption-policy-granularity-141ac283b9c0778e.yaml b/releasenotes/notes/volume-type-encryption-policy-granularity-141ac283b9c0778e.yaml
new file mode 100644
index 0000000..4aeb107
--- /dev/null
+++ b/releasenotes/notes/volume-type-encryption-policy-granularity-141ac283b9c0778e.yaml
@@ -0,0 +1,19 @@
+---
+features:
+ - |
+ Added new Cinder feature flag (``CONF.policy_feature_enabled.added_cinder_policies_stein``)
+ for the following newly introduced granular Cinder policies:
+
+ - ``volume_extension:volume_type_encryption:create``
+ - ``volume_extension:volume_type_encryption:get``
+ - ``volume_extension:volume_type_encryption:update``
+ - ``volume_extension:volume_type_encryption:delete``
+
+ The corresponding Patrole test cases are modified to support
+ the granularity. The test cases also support backward
+ compatibility with the old single rule:
+ ``volume_extension:volume_type_encryption``
+
+ The ``rules`` parameter in ``rbac_rule_validation.action``
+ decorator now also accepts a list of callables; each callable
+ should return a policy action (str).
diff --git a/releasenotes/notes/yaml-policy-file-support-278d3edf64f98d69.yaml b/releasenotes/notes/yaml-policy-file-support-278d3edf64f98d69.yaml
new file mode 100644
index 0000000..e333377
--- /dev/null
+++ b/releasenotes/notes/yaml-policy-file-support-278d3edf64f98d69.yaml
@@ -0,0 +1,7 @@
+---
+features:
+- |
+ Patrole now supports parsing custom YAML policy files, the new policy file
+ extension since Ocata. The function ``_get_policy_data`` has been renamed to
+ ``get_rules`` and been changed to re-use ``oslo_policy.policy.Rules.load``
+ function.
diff --git a/releasenotes/source/index.rst b/releasenotes/source/index.rst
index ce29994..eb061a4 100644
--- a/releasenotes/source/index.rst
+++ b/releasenotes/source/index.rst
@@ -6,6 +6,7 @@
:maxdepth: 1
unreleased
+ v0.4.0
v0.3.0
v0.2.0
v0.1.0
diff --git a/releasenotes/source/v0.4.0.rst b/releasenotes/source/v0.4.0.rst
new file mode 100644
index 0000000..2ed32ff
--- /dev/null
+++ b/releasenotes/source/v0.4.0.rst
@@ -0,0 +1,6 @@
+====================
+v0.4.0 Release Notes
+====================
+
+.. release-notes:: 0.4.0 Release Notes
+ :version: 0.4.0
diff --git a/test-requirements.txt b/test-requirements.txt
index 9085c07..a08c27a 100644
--- a/test-requirements.txt
+++ b/test-requirements.txt
@@ -8,3 +8,4 @@
nose>=1.3.7 # LGPL
nosexcover>=1.0.10 # BSD
oslotest>=3.2.0 # Apache-2.0
+bandit>=1.5 # Apache-2.0
diff --git a/tox.ini b/tox.ini
index a09822f..ea9abf1 100644
--- a/tox.ini
+++ b/tox.ini
@@ -22,8 +22,12 @@
[testenv:pep8]
basepython = python3
-commands = flake8 {posargs}
- check-uuid --package patrole_tempest_plugin.tests.api
+deps =
+ -r{toxinidir}/test-requirements.txt
+commands =
+ flake8 {posargs}
+ bandit -r patrole_tempest_plugin -x patrole_tempest_plugin/tests -n 5
+ check-uuid --package patrole_tempest_plugin.tests.api
[testenv:uuidgen]
basepython = python3