Merge "docs: Add multi-policy validation documentation"
diff --git a/.zuul.yaml b/.zuul.yaml
index fb110f0..edd812c 100644
--- a/.zuul.yaml
+++ b/.zuul.yaml
@@ -10,11 +10,14 @@
timeout: 7800
roles:
- zuul: openstack-dev/devstack
- irrelevant-files:
+ # Define common irrelevant files to use everywhere else
+ irrelevant-files: &patrole-irrelevant-files
- ^(test-|)requirements.txt$
- ^.*\.rst$
- ^doc/.*
+ - ^etc/.*$
- ^patrole/patrole_tempest_plugin/tests/unit/.*$
+ - ^patrole/patrole_tempest_plugin/hacking/.*$
- ^releasenotes/.*
- ^setup.cfg$
vars:
@@ -28,7 +31,8 @@
neutron-trunk: true
tempest_concurrency: 2
tempest_test_regex: (?!.*\[.*\bslow\b.*\])(^patrole_tempest_plugin\.tests\.api)
- tox_envlist: all-plugin
+ tox_envlist: all
+ tox_extra_args: --sitepackages
- job:
name: patrole-base-multinode
@@ -46,13 +50,7 @@
- openstack-infra/devstack-gate
- openstack/tempest
- openstack/patrole
- irrelevant-files:
- - ^(test-|)requirements.txt$
- - ^.*\.rst$
- - ^doc/.*
- - ^patrole/patrole_tempest_plugin/tests/unit/.*$
- - ^releasenotes/.*
- - ^setup.cfg$
+ irrelevant-files: *patrole-irrelevant-files
vars:
devstack_localrc:
TEMPEST_PLUGINS: "'{{ ansible_user_dir }}/src/git.openstack.org/openstack/patrole'"
@@ -63,7 +61,8 @@
neutron: true
tempest_concurrency: 1
tempest_test_regex: (?=.*\[.*\bslow\b.*\])(^patrole_tempest_plugin\.tests\.api)
- tox_envlist: all-plugin
+ tox_envlist: all
+ tox_extra_args: --sitepackages
- job:
name: patrole-admin
@@ -71,7 +70,7 @@
description: Patrole job for admin role.
vars:
devstack_localrc:
- RBAC_TEST_ROLE: admin
+ RBAC_TEST_ROLES: admin
- job:
name: patrole-member
@@ -84,7 +83,7 @@
- stable/pike
vars:
devstack_localrc:
- RBAC_TEST_ROLE: member
+ RBAC_TEST_ROLES: member
- job:
name: patrole-member-rocky
@@ -107,7 +106,7 @@
voting: false
vars:
devstack_localrc:
- RBAC_TEST_ROLE: admin
+ RBAC_TEST_ROLES: admin
- job:
name: patrole-multinode-member
@@ -115,7 +114,7 @@
voting: false
vars:
devstack_localrc:
- RBAC_TEST_ROLE: member
+ RBAC_TEST_ROLES: member
- job:
name: patrole-py35-member
@@ -125,7 +124,7 @@
devstack_localrc:
# Use member for py35 because arguably negative testing is more
# important than admin, which is already covered by patrole-admin job.
- RBAC_TEST_ROLE: member
+ RBAC_TEST_ROLES: member
USE_PYTHON3: true
devstack_services:
s-account: false
@@ -164,7 +163,7 @@
voting: false
vars:
devstack_localrc:
- RBAC_TEST_ROLE: member
+ RBAC_TEST_ROLES: member
tempest_test_regex: (?=.*PluginRbacTest)(^patrole_tempest_plugin\.tests\.api)
- job:
@@ -173,7 +172,7 @@
voting: false
vars:
devstack_localrc:
- RBAC_TEST_ROLE: admin
+ RBAC_TEST_ROLES: admin
tempest_test_regex: (?=.*PluginRbacTest)(^patrole_tempest_plugin\.tests\.api)
- project:
diff --git a/HACKING.rst b/HACKING.rst
index 28a977d..87e3b1f 100644
--- a/HACKING.rst
+++ b/HACKING.rst
@@ -33,12 +33,15 @@
The following are Patrole's specific Commandments:
- [P100] The ``rbac_rule_validation.action`` decorator must be applied to
- an RBAC test
+ all RBAC tests
- [P101] RBAC test filenames must end with "_rbac.py"; for example,
test_servers_rbac.py, not test_servers.py
- [P102] RBAC test class names must end in 'RbacTest'
- [P103] ``self.client`` must not be used as a client alias; this allows for
code that is more maintainable and easier to read
+- [P104] RBAC `plugin test class`_ names must end in 'PluginRbacTest'
+
+.. _plugin test class: https://github.com/openstack/patrole/tree/master/patrole_tempest_plugin/tests/api/network#neutron-plugin-tests
Role Overriding
---------------
diff --git a/README.rst b/README.rst
index fdcbc6b..31cd3b7 100644
--- a/README.rst
+++ b/README.rst
@@ -153,10 +153,11 @@
will run the same set of tests as the default gate jobs.
- You can also run Patrole tests using `tox`_. To do so, ``cd`` into the
+ You can also run Patrole tests using `tox`_, but as Patrole needs access to
+ global packages use ``--sitepackages`` argument. To do so, ``cd`` into the
**Tempest** directory and run::
- $ tox -eall-plugin -- patrole_tempest_plugin.tests.api
+ $ tox -eall --sitepackages -- patrole_tempest_plugin.tests.api
.. note::
@@ -186,20 +187,20 @@
RBAC Tests
----------
-To change the role that the patrole tests are being run as, edit
-``rbac_test_role`` in the ``patrole`` section of tempest.conf: ::
+To change the roles that the patrole tests are being run as, edit
+``rbac_test_roles`` in the ``patrole`` section of tempest.conf: ::
[patrole]
- rbac_test_role = member
+ rbac_test_role = member,reader
...
.. note::
- The ``rbac_test_role`` is service-specific. member, for example,
+ The ``rbac_test_roles`` is service-specific. member, for example,
is an arbitrary role, but by convention is used to designate the default
non-admin role in the system. Most Patrole tests should be run with
**admin** and **member** roles. However, other services may use entirely
- different roles.
+ different roles or role combinations.
For more information about the member role and its nomenclature,
please see: `<https://ask.openstack.org/en/question/4759/member-vs-_member_/>`__.
diff --git a/devstack/plugin.sh b/devstack/plugin.sh
index a6259f4..6b95182 100644
--- a/devstack/plugin.sh
+++ b/devstack/plugin.sh
@@ -14,9 +14,14 @@
setup_package $PATROLE_DIR -e
if [[ ${DEVSTACK_SERIES} == 'pike' ]]; then
- if [[ "$RBAC_TEST_ROLE" == "member" ]]; then
- RBAC_TEST_ROLE="Member"
- fi
+ IFS=',' read -ra roles_array <<< "$RBAC_TEST_ROLES"
+ RBAC_TEST_ROLES=""
+ for i in "${roles_array[@]}"; do
+ if [[ $i == "member" ]]; then
+ i="Member"
+ fi
+ RBAC_TEST_ROLES="$i,$RBAC_TEST_ROLES"
+ done
# Policies used by Patrole testing that were changed in a backwards-incompatible way.
# TODO(felipemonteiro): Remove these once stable/pike becomes EOL.
@@ -27,31 +32,37 @@
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
+ # TODO(cl566n): Remove these once stable/pike becomes EOL.
# 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 removed_keystone_policies_stein False
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
+ IFS=',' read -ra roles_array <<< "$RBAC_TEST_ROLES"
+ RBAC_TEST_ROLES=""
+ for i in "${roles_array[@]}"; do
+ if [[ $i == "member" ]]; then
+ i="Member"
+ fi
+ RBAC_TEST_ROLES="$i,$RBAC_TEST_ROLES"
+ done
+ # TODO(cl566n): Remove these once stable/queens becomes EOL.
# 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 removed_keystone_policies_stein False
iniset $TEMPEST_CONFIG policy-feature-enabled added_cinder_policies_stein False
fi
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
+ iniset $TEMPEST_CONFIG policy-feature-enabled removed_keystone_policies_stein False
fi
- iniset $TEMPEST_CONFIG patrole rbac_test_role $RBAC_TEST_ROLE
+ iniset $TEMPEST_CONFIG patrole rbac_test_roles $RBAC_TEST_ROLES
}
if is_service_enabled tempest; then
diff --git a/doc/source/configuration.rst b/doc/source/configuration.rst
index f6aaf04..05716fe 100644
--- a/doc/source/configuration.rst
+++ b/doc/source/configuration.rst
@@ -7,34 +7,28 @@
file. All Patrole-specific configuration options should be included under
the ``patrole`` group.
-RBAC Test Role
---------------
+RBAC Test Roles
+---------------
-The RBAC test role governs which role is used when running Patrole tests. For
-example, setting ``rbac_test_role`` to "admin" will execute all RBAC tests
-using admin credentials. Changing the ``rbac_test_role`` value will `override`
-Tempest's primary credentials to use that role.
+The RBAC test roles govern the list of roles to be used when running Patrole
+tests. For example, setting ``rbac_test_roles`` to "admin" will execute all
+RBAC tests using admin credentials. Changing the ``rbac_test_roles`` value
+will `override` Tempest's primary credentials to use that role.
-This implies that, if ``rbac_test_role`` is "admin", regardless of the Tempest
+This implies that, if ``rbac_test_roles`` is "admin", regardless of the Tempest
credentials used by a client, the client will be calling APIs using the admin
role. That is, ``self.os_primary.servers_client`` will run as though it were
``self.os_admin.servers_client``.
-Similarly, setting ``rbac_test_role`` to a non-admin role results in Tempest's
-primary credentials being overridden by the role specified by
-``rbac_test_role``.
+Similarly, setting ``rbac_test_roles`` with various roles, results in
+Tempest's primary credentials being overridden by the roles specified by
+``rbac_test_roles``.
.. note::
- Only the role of the primary Tempest credentials ("os_primary") is
+ Only the roles of the primary Tempest credentials ("os_primary") are
modified. The ``user_id`` and ``project_id`` remain unchanged.
-Enable RBAC
------------
-
-Given the value of ``enable_rbac``, enables or disables Patrole tests. If
-``enable_rbac`` is ``False``, then Patrole tests are skipped.
-
Custom Policy Files
-------------------
diff --git a/doc/source/framework/overview.rst b/doc/source/framework/overview.rst
index 8e04082..6f72eec 100644
--- a/doc/source/framework/overview.rst
+++ b/doc/source/framework/overview.rst
@@ -10,10 +10,10 @@
RBAC testing validation is broken up into 3 stages:
#. "Expected" stage. Determine whether the test should be able to succeed
- or fail based on the test role defined by ``[patrole] rbac_test_role``)
+ or fail based on the test roles defined by ``[patrole] rbac_test_roles``)
and the policy action that the test enforces.
#. "Actual" stage. Run the test by calling the API endpoint that enforces
- the expected policy action using the test role.
+ the expected policy action using the test roles.
#. Comparing the outputs from both stages for consistency. A "consistent"
result is treated as a pass and an "inconsistent" result is treated
as a failure. "Consistent" (or successful) cases include:
@@ -63,7 +63,7 @@
---------------------------
Module called by :ref:`rbac-validation` to verify whether the test
-role is allowed to execute a policy action by querying ``oslo.policy`` with
+roles are allowed to execute a policy action by querying ``oslo.policy`` with
required test data. The result is used by :ref:`rbac-validation` as the
"Expected" result.
diff --git a/doc/source/test_writing_guide.rst b/doc/source/test_writing_guide.rst
index 1291201..4e0f0be 100644
--- a/doc/source/test_writing_guide.rst
+++ b/doc/source/test_writing_guide.rst
@@ -23,8 +23,8 @@
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.
+action that authorizes the policy under test, by manipulating roles of the
+Tempest credentials.
Patrole implicitly splits up each test into 3 stages: set up, test execution,
and teardown.
@@ -33,10 +33,10 @@
#. 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
+#. Test execution: ``[patrole] rbac_test_roles`` 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``.
+ credentials overridden with the ``[patrole] rbac_test_roles``.
#. Teardown: Admin role is used automatically. The primary credentials have
been overridden with the admin role.
diff --git a/etc/patrole.conf.sample b/etc/patrole.conf.sample
index 6de073d..6433f40 100644
--- a/etc/patrole.conf.sample
+++ b/etc/patrole.conf.sample
@@ -7,12 +7,17 @@
# From patrole.config
#
-# The current RBAC role against which to run Patrole
-# tests. (string value)
-#rbac_test_role = admin
+# DEPRECATED: The current RBAC role against which to run
+# Patrole tests. (string value)
+# This option is deprecated for removal.
+# Its value may be silently ignored in the future.
+# Reason: This option is deprecated and being
+# replaced with ``rbac_test_roles``.
+#rbac_test_role =
-# Enables RBAC tests. (boolean value)
-#enable_rbac = true
+# The current RBAC roles to be assigned to Keystone
+# Group against which to run Patrole tests. (list value)
+#rbac_test_roles = admin
# List of the paths to search for policy files. Each
# policy path assumes that the service name is included in the path
@@ -20,9 +25,11 @@
# 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.
+# 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.
# (list value)
#custom_policy_files = /etc/%s/policy.json
@@ -150,3 +157,16 @@
# This policy
# was changed in a backwards-incompatible way. (boolean value)
#volume_extension_volume_actions_unreserve_policy = true
+
+# 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. (boolean value)
+#removed_nova_policies_stein = true
+
+# Are the Cinder API extension policies available in the
+# cloud (e.g. [create|update|get|delete]_encryption_policy)? These
+# policies are
+# added in Stein. (boolean value)
+#added_cinder_policies_stein = true
diff --git a/patrole_tempest_plugin/config.py b/patrole_tempest_plugin/config.py
index 56a786b..90d5fba 100644
--- a/patrole_tempest_plugin/config.py
+++ b/patrole_tempest_plugin/config.py
@@ -22,8 +22,16 @@
PatroleGroup = [
cfg.StrOpt('rbac_test_role',
default='admin',
+ deprecated_for_removal=True,
+ deprecated_reason="""This option is deprecated and being
+replaced with ``rbac_test_roles``.
+""",
help="""The current RBAC role against which to run
Patrole tests."""),
+ cfg.ListOpt('rbac_test_roles',
+ help="""List of the RBAC roles against which to run
+Patrole tests.""",
+ default=['admin']),
cfg.ListOpt('custom_policy_files',
default=['/etc/%s/policy.json'],
help="""List of the paths to search for policy files. Each
@@ -162,11 +170,16 @@
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('removed_keystone_policies_stein',
+ default=True,
+ help="""Are the obsolete Keystone policies available in the
+cloud (e.g. identity:[create|update|get|delete]_credential)? These policies
+were removed in Stein."""),
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.""")
+ help="""Are the Cinder Stein 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/hacking/checks.py b/patrole_tempest_plugin/hacking/checks.py
index d106da8..1f06258 100644
--- a/patrole_tempest_plugin/hacking/checks.py
+++ b/patrole_tempest_plugin/hacking/checks.py
@@ -36,6 +36,8 @@
RULE_VALIDATION_DECORATOR = re.compile(
r'\s*@rbac_rule_validation.action\(.*')
IDEMPOTENT_ID_DECORATOR = re.compile(r'\s*@decorators\.idempotent_id\((.*)\)')
+PLUGIN_RBAC_TEST = re.compile(
+ r"class .+\(.+PluginRbacTest\)|class .+PluginRbacTest\(.+\)")
have_rbac_decorator = False
@@ -211,6 +213,44 @@
return 0, "Do not use 'self.client' as a service client alias"
+def no_plugin_rbac_test_suffix_in_plugin_test_class_name(physical_line,
+ filename):
+ """Check that Plugin RBAC class names end with "PluginRbacTest"
+
+ P104
+ """
+ suffix = "PluginRbacTest"
+ if "patrole_tempest_plugin/tests/api" in filename:
+ if PLUGIN_RBAC_TEST.match(physical_line):
+ subclass, superclass = physical_line.split('(')
+ subclass = subclass.split('class')[1].strip()
+ superclass = superclass.split(')')[0].strip()
+ if "." in superclass:
+ superclass = superclass.split(".")[1]
+
+ both_have = all(
+ clazz.endswith(suffix) for clazz in [subclass, superclass])
+ none_have = not any(
+ clazz.endswith(suffix) for clazz in [subclass, superclass])
+
+ if not (both_have or none_have):
+ if (subclass.startswith("Base") and
+ superclass.startswith("Base")):
+ return
+
+ # Case 1: Subclass of "BasePluginRbacTest" must end in `suffix`
+ # Case 2: Subclass that ends in `suffix` must inherit from base
+ # class ending in `suffix`.
+ if not subclass.endswith(suffix):
+ error = ("Plugin RBAC test subclasses must end in "
+ "'PluginRbacTest'")
+ return len(subclass) - 1, error
+ elif not superclass.endswith(suffix):
+ error = ("Plugin RBAC test subclasses must inherit from a "
+ "'PluginRbacTest' base class")
+ return len(superclass) - 1, error
+
+
def factory(register):
register(import_no_clients_in_api_tests)
register(no_setup_teardown_class_for_tests)
@@ -223,3 +263,4 @@
register(no_rbac_rule_validation_decorator)
register(no_rbac_suffix_in_test_filename)
register(no_rbac_test_suffix_in_test_class_name)
+ register(no_plugin_rbac_test_suffix_in_plugin_test_class_name)
diff --git a/patrole_tempest_plugin/policy_authority.py b/patrole_tempest_plugin/policy_authority.py
index 2a49b6c..e0a26a3 100644
--- a/patrole_tempest_plugin/policy_authority.py
+++ b/patrole_tempest_plugin/policy_authority.py
@@ -154,17 +154,17 @@
if os.path.isfile(filename):
cls.policy_files[service].append(filename)
- def allowed(self, rule_name, role):
+ def allowed(self, rule_name, roles):
"""Checks if a given rule in a policy is allowed with given role.
:param string rule_name: Policy name to pass to``oslo.policy``.
- :param string role: Role to validate for authorization.
+ :param List[string] roles: List of roles to validate for authorization.
:raises RbacParsingException: If ``rule_name`` does not exist in the
cloud (in policy file or among registered in-code policy defaults).
"""
- is_admin_context = self._is_admin_context(role)
+ is_admin_context = self._is_admin_context(roles)
is_allowed = self._allowed(
- access=self._get_access_token(role),
+ access=self._get_access_token(roles),
apply_rule=rule_name,
is_admin=is_admin_context)
return is_allowed
@@ -224,7 +224,7 @@
return rules
- def _is_admin_context(self, role):
+ def _is_admin_context(self, roles):
"""Checks whether a role has admin context.
If context_is_admin is contained in the policy file, then checks
@@ -233,17 +233,17 @@
"""
if 'context_is_admin' in self.rules.keys():
return self._allowed(
- access=self._get_access_token(role),
+ access=self._get_access_token(roles),
apply_rule='context_is_admin')
- return role == CONF.identity.admin_role
+ return CONF.identity.admin_role in roles
- def _get_access_token(self, role):
+ def _get_access_token(self, roles):
access_token = {
"token": {
"roles": [
{
"name": role
- }
+ } for role in roles
],
"project_id": self.project_id,
"tenant_id": self.project_id,
diff --git a/patrole_tempest_plugin/rbac_exceptions.py b/patrole_tempest_plugin/rbac_exceptions.py
index 6bdd7df..ad697b0 100644
--- a/patrole_tempest_plugin/rbac_exceptions.py
+++ b/patrole_tempest_plugin/rbac_exceptions.py
@@ -20,16 +20,34 @@
message = "An unknown RBAC exception occurred"
-class RbacMalformedResponse(BasePatroleException):
- message = ("The response body is missing the expected %(attribute)s due "
- "to policy enforcement failure.")
+class BasePatroleResponseBodyException(BasePatroleException):
+ message = "Response body incomplete due to RBAC authorization failure"
- def __init__(self, empty=False, **kwargs):
- if empty:
- self.message = ("The response body is empty due to policy "
- "enforcement failure.")
- kwargs = {}
- super(RbacMalformedResponse, self).__init__(**kwargs)
+
+class RbacMissingAttributeResponseBody(BasePatroleResponseBodyException):
+ """Raised when a list or show action is missing an attribute following
+ RBAC authorization failure.
+ """
+ message = ("The response body is missing the expected %(attribute)s due "
+ "to policy enforcement failure")
+
+
+class RbacPartialResponseBody(BasePatroleResponseBodyException):
+ """Raised when a list action only returns a subset of the available
+ resources.
+
+ For example, admin can return more resources than member for a list action.
+ """
+ message = ("The response body only lists a subset of the available "
+ "resources due to partial policy enforcement failure. Response "
+ "body: %(body)s")
+
+
+class RbacEmptyResponseBody(BasePatroleResponseBodyException):
+ """Raised when a list or show action is empty following RBAC authorization
+ failure.
+ """
+ message = ("The response body is empty due to policy enforcement failure.")
class RbacResourceSetupFailed(BasePatroleException):
diff --git a/patrole_tempest_plugin/rbac_rule_validation.py b/patrole_tempest_plugin/rbac_rule_validation.py
index c85376f..9ca437b 100644
--- a/patrole_tempest_plugin/rbac_rule_validation.py
+++ b/patrole_tempest_plugin/rbac_rule_validation.py
@@ -17,6 +17,7 @@
import logging
import sys
+from oslo_log import versionutils
from oslo_utils import excutils
import six
@@ -47,7 +48,7 @@
* an OpenStack service,
* a policy action (``rule``) enforced by that service, and
- * the test role defined by ``[patrole] rbac_test_role``
+ * the test roles defined by ``[patrole] rbac_test_roles``
determines whether the test role has sufficient permissions to perform an
API call that enforces the ``rule``.
@@ -142,7 +143,15 @@
expected_error_codes)
def decorator(test_func):
- role = CONF.patrole.rbac_test_role
+ roles = CONF.patrole.rbac_test_roles
+ # TODO(vegasq) drop once CONF.patrole.rbac_test_role is removed
+ if CONF.patrole.rbac_test_role:
+ msg = ('CONF.patrole.rbac_test_role is deprecated in favor of '
+ 'CONF.patrole.rbac_test_roles and will be removed in '
+ 'future.')
+ versionutils.report_deprecated_feature(LOG, msg)
+ if not roles:
+ roles.append(CONF.patrole.rbac_test_role)
@functools.wraps(test_func)
def wrapper(*args, **kwargs):
@@ -189,7 +198,8 @@
test_status = ('Error, %s' % (msg))
LOG.error(msg)
except (expected_exception,
- rbac_exceptions.RbacMalformedResponse) as actual_exception:
+ rbac_exceptions.BasePatroleResponseBodyException) \
+ as actual_exception:
caught_exception = actual_exception
test_status = 'Denied'
@@ -200,10 +210,10 @@
service)
if allowed:
- msg = ("Role %s was not allowed to perform the following "
- "actions: %s. Expected allowed actions: %s. "
- "Expected disallowed actions: %s." % (
- role, sorted(rules),
+ msg = ("User with roles %s was not allowed to perform the "
+ "following actions: %s. Expected allowed actions: "
+ "%s. Expected disallowed actions: %s." % (
+ roles, sorted(rules),
sorted(set(rules) - set(disallowed_rules)),
sorted(disallowed_rules)))
LOG.error(msg)
@@ -236,7 +246,7 @@
msg = (
"OverPermission: Role %s was allowed to perform the "
"following disallowed actions: %s" % (
- role, sorted(disallowed_rules)
+ roles, sorted(disallowed_rules)
)
)
LOG.error(msg)
@@ -328,7 +338,12 @@
LOG.error(msg)
raise rbac_exceptions.RbacResourceSetupFailed(msg)
- role = CONF.patrole.rbac_test_role
+ roles = CONF.patrole.rbac_test_roles
+ # TODO(vegasq) drop once CONF.patrole.rbac_test_role is removed
+ if CONF.patrole.rbac_test_role:
+ if not roles:
+ roles.append(CONF.patrole.rbac_test_role)
+
# Test RBAC against custom requirements. Otherwise use oslo.policy.
if CONF.patrole.test_custom_requirements:
authority = requirements_authority.RequirementsAuthority(
@@ -339,14 +354,14 @@
authority = policy_authority.PolicyAuthority(
project_id, user_id, service,
extra_target_data=formatted_target_data)
- is_allowed = authority.allowed(rule, role)
+ is_allowed = authority.allowed(rule, roles)
if is_allowed:
LOG.debug("[Policy action]: %s, [Role]: %s is allowed!", rule,
- role)
+ roles)
else:
LOG.debug("[Policy action]: %s, [Role]: %s is NOT allowed!",
- rule, role)
+ rule, roles)
return is_allowed
diff --git a/patrole_tempest_plugin/rbac_utils.py b/patrole_tempest_plugin/rbac_utils.py
index 6db2199..33955c3 100644
--- a/patrole_tempest_plugin/rbac_utils.py
+++ b/patrole_tempest_plugin/rbac_utils.py
@@ -23,6 +23,7 @@
from tempest import clients
from tempest.common import credentials_factory as credentials
from tempest import config
+from tempest.lib import exceptions as lib_exc
from patrole_tempest_plugin import rbac_exceptions
@@ -39,7 +40,7 @@
up, and primary credentials, needed to perform the API call which does
policy enforcement. The primary credentials always cycle between roles
defined by ``CONF.identity.admin_role`` and
- ``CONF.patrole.rbac_test_role``.
+ ``CONF.patrole.rbac_test_roles``.
"""
def __init__(self, test_obj):
@@ -50,16 +51,22 @@
# Intialize the admin roles_client to perform role switching.
admin_mgr = clients.Manager(
credentials.get_configured_admin_credentials())
- if test_obj.get_identity_version() == 'v3':
+ if CONF.identity_feature_enabled.api_v3:
admin_roles_client = admin_mgr.roles_v3_client
else:
- admin_roles_client = admin_mgr.roles_client
+ raise lib_exc.InvalidConfiguration(
+ "Patrole role overriding only supports v3 identity API.")
self.admin_roles_client = admin_roles_client
+
+ self.user_id = test_obj.os_primary.credentials.user_id
+ self.project_id = test_obj.os_primary.credentials.tenant_id
+
+ # Change default role to admin
self._override_role(test_obj, False)
admin_role_id = None
- rbac_role_id = None
+ rbac_role_ids = None
@contextmanager
def override_role(self, test_obj):
@@ -67,7 +74,7 @@
Temporarily change the role used by ``os_primary`` credentials to:
- * ``[patrole] rbac_test_role`` before test execution
+ * ``[patrole] rbac_test_roles`` before test execution
* ``[identity] admin_role`` after test execution
Automatically switches to admin role after test execution.
@@ -120,25 +127,21 @@
* If True: role is set to ``[patrole] rbac_test_role``
* If False: role is set to ``[identity] admin_role``
"""
- self.user_id = test_obj.os_primary.credentials.user_id
- self.project_id = test_obj.os_primary.credentials.tenant_id
- self.token = test_obj.os_primary.auth_provider.get_token()
-
LOG.debug('Overriding role to: %s.', toggle_rbac_role)
- role_already_present = False
+ roles_already_present = False
try:
- if not all([self.admin_role_id, self.rbac_role_id]):
+ if not all([self.admin_role_id, self.rbac_role_ids]):
self._get_roles_by_name()
- target_role = (
- self.rbac_role_id if toggle_rbac_role else self.admin_role_id)
- role_already_present = self._list_and_clear_user_roles_on_project(
- target_role)
+ target_roles = (self.rbac_role_ids
+ if toggle_rbac_role else [self.admin_role_id])
+ roles_already_present = self._list_and_clear_user_roles_on_project(
+ target_roles)
# Do not override roles if `target_role` already exists.
- if not role_already_present:
- self._create_user_role_on_project(target_role)
+ if not roles_already_present:
+ self._create_user_role_on_project(target_roles)
except Exception as exp:
with excutils.save_and_reraise_exception():
LOG.exception(exp)
@@ -150,8 +153,8 @@
# passing the second boundary before attempting to authenticate.
# Only sleep if a token revocation occurred as a result of role
# overriding. This will optimize test runtime in the case where
- # ``[identity] admin_role`` == ``[patrole] rbac_test_role``.
- if not role_already_present:
+ # ``[identity] admin_role`` == ``[patrole] rbac_test_roles``.
+ if not roles_already_present:
time.sleep(1)
for provider in auth_providers:
@@ -162,41 +165,53 @@
role_map = {r['name']: r['id'] for r in available_roles}
LOG.debug('Available roles: %s', list(role_map.keys()))
- admin_role_id = role_map.get(CONF.identity.admin_role)
- rbac_role_id = role_map.get(CONF.patrole.rbac_test_role)
+ rbac_role_ids = []
+ roles = CONF.patrole.rbac_test_roles
+ # TODO(vegasq) drop once CONF.patrole.rbac_test_role is removed
+ if CONF.patrole.rbac_test_role:
+ if not roles:
+ roles.append(CONF.patrole.rbac_test_role)
- if not all([admin_role_id, rbac_role_id]):
+ for role_name in roles:
+ rbac_role_ids.append(role_map.get(role_name))
+
+ admin_role_id = role_map.get(CONF.identity.admin_role)
+
+ if not all([admin_role_id, all(rbac_role_ids)]):
missing_roles = []
- msg = ("Could not find `[patrole] rbac_test_role` or "
+ msg = ("Could not find `[patrole] rbac_test_roles` or "
"`[identity] admin_role`, both of which are required for "
"RBAC testing.")
if not admin_role_id:
missing_roles.append(CONF.identity.admin_role)
- if not rbac_role_id:
- missing_roles.append(CONF.patrole.rbac_test_role)
+ if not all(rbac_role_ids):
+ missing_roles += [role_name for role_name in roles
+ if not role_map.get(role_name)]
+
msg += " Following roles were not found: %s." % (
", ".join(missing_roles))
msg += " Available roles: %s." % ", ".join(list(role_map.keys()))
raise rbac_exceptions.RbacResourceSetupFailed(msg)
self.admin_role_id = admin_role_id
- self.rbac_role_id = rbac_role_id
+ self.rbac_role_ids = rbac_role_ids
- def _create_user_role_on_project(self, role_id):
- self.admin_roles_client.create_user_role_on_project(
- self.project_id, self.user_id, role_id)
+ def _create_user_role_on_project(self, role_ids):
+ for role_id in role_ids:
+ self.admin_roles_client.create_user_role_on_project(
+ self.project_id, self.user_id, role_id)
- def _list_and_clear_user_roles_on_project(self, role_id):
+ def _list_and_clear_user_roles_on_project(self, role_ids):
roles = self.admin_roles_client.list_user_roles_on_project(
self.project_id, self.user_id)['roles']
- role_ids = [role['id'] for role in roles]
+ all_role_ids = [role['id'] for role in roles]
- # NOTE(felipemonteiro): We do not use ``role_id in role_ids`` here to
- # avoid over-permission errors: if the current list of roles on the
+ # NOTE(felipemonteiro): We do not use ``role_id in all_role_ids`` here
+ # to avoid over-permission errors: if the current list of roles on the
# project includes "admin" and "Member", and we are switching to the
# "Member" role, then we must delete the "admin" role. Thus, we only
# return early if the user's roles on the project are an exact match.
- if [role_id] == role_ids:
+ if set(role_ids) == set(all_role_ids):
return True
for role in roles:
@@ -277,8 +292,14 @@
def is_admin():
"""Verifies whether the current test role equals the admin role.
- :returns: True if ``rbac_test_role`` is the admin role.
+ :returns: True if ``rbac_test_roles`` contain the admin role.
"""
+ roles = CONF.patrole.rbac_test_roles
+ # TODO(vegasq) drop once CONF.patrole.rbac_test_role is removed
+ if CONF.patrole.rbac_test_role:
+ roles.append(CONF.patrole.rbac_test_role)
+ roles = list(set(roles))
+
# TODO(felipemonteiro): Make this more robust via a context is admin
# lookup.
- return CONF.patrole.rbac_test_role == CONF.identity.admin_role
+ return CONF.identity.admin_role in roles
diff --git a/patrole_tempest_plugin/requirements_authority.py b/patrole_tempest_plugin/requirements_authority.py
index 75df9f4..57caf79 100644
--- a/patrole_tempest_plugin/requirements_authority.py
+++ b/patrole_tempest_plugin/requirements_authority.py
@@ -95,13 +95,14 @@
else:
self.roles_dict = None
- def allowed(self, rule_name, role):
+ def allowed(self, rule_name, roles):
"""Checks if a given rule in a policy is allowed with given role.
:param string rule_name: Rule to be checked using provided requirements
file specified by ``[patrole].custom_requirements_file``. Must be
a key present in this file, under the appropriate component.
- :param string role: Role to validate against custom requirements file.
+ :param List[string] roles: Roles to validate against custom
+ requirements file.
:returns: True if ``role`` is allowed to perform ``rule_name``, else
False.
:rtype: bool
@@ -115,8 +116,7 @@
"formatted.")
try:
_api = self.roles_dict[rule_name]
- return role in _api
+ return all(role in _api for role in roles)
except KeyError:
raise KeyError("'%s' API is not defined in the requirements YAML "
"file" % rule_name)
- return False
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 317c1ad..8d4d70f 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
@@ -50,7 +50,7 @@
expected_attr = 'os-flavor-access:is_public'
if expected_attr not in body:
- raise rbac_exceptions.RbacMalformedResponse(
+ raise rbac_exceptions.RbacMissingAttributeResponseBody(
attribute=expected_attr)
@testtools.skipIf(CONF.policy_feature_enabled.removed_nova_policies_stein,
@@ -71,7 +71,7 @@
# If the `expected_attr` was not found in any flavor, then policy
# enforcement failed.
if not public_flavors:
- raise rbac_exceptions.RbacMalformedResponse(
+ raise rbac_exceptions.RbacMissingAttributeResponseBody(
attribute=expected_attr)
@decorators.idempotent_id('39cb5c8f-9990-436f-9282-fc76a41d9bac')
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 0748e67..cbb2e19 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
@@ -45,7 +45,7 @@
with self.rbac_utils.override_role(self):
result = self.flavors_client.list_flavors(detail=True)['flavors']
if 'rxtx_factor' not in result[0]:
- raise rbac_exceptions.RbacMalformedResponse(
+ raise rbac_exceptions.RbacMissingAttributeResponseBody(
attribute='rxtx_factor')
@testtools.skipIf(CONF.policy_feature_enabled.removed_nova_policies_stein,
@@ -59,5 +59,5 @@
result = self.flavors_client.show_flavor(
CONF.compute.flavor_ref)['flavor']
if 'rxtx_factor' not in result:
- raise rbac_exceptions.RbacMalformedResponse(
+ raise rbac_exceptions.RbacMissingAttributeResponseBody(
attribute='rxtx_factor')
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 f6c1b67..e16222c 100644
--- a/patrole_tempest_plugin/tests/api/compute/test_images_rbac.py
+++ b/patrole_tempest_plugin/tests/api/compute/test_images_rbac.py
@@ -294,7 +294,7 @@
expected_attr = 'OS-EXT-IMG-SIZE:size'
if expected_attr not in body:
- raise rbac_exceptions.RbacMalformedResponse(
+ raise rbac_exceptions.RbacMissingAttributeResponseBody(
attribute=expected_attr)
@testtools.skipIf(CONF.policy_feature_enabled.removed_nova_policies_stein,
@@ -310,5 +310,5 @@
expected_attr = 'OS-EXT-IMG-SIZE:size'
if expected_attr not in body[0]:
- raise rbac_exceptions.RbacMalformedResponse(
+ raise rbac_exceptions.RbacMissingAttributeResponseBody(
attribute=expected_attr)
diff --git a/patrole_tempest_plugin/tests/api/compute/test_quota_class_sets_rbac.py b/patrole_tempest_plugin/tests/api/compute/test_quota_class_sets_rbac.py
index 52c8b3d..201922c 100644
--- a/patrole_tempest_plugin/tests/api/compute/test_quota_class_sets_rbac.py
+++ b/patrole_tempest_plugin/tests/api/compute/test_quota_class_sets_rbac.py
@@ -13,6 +13,7 @@
# License for the specific language governing permissions and limitations
# under the License.
+from tempest.common import identity
from tempest.common import tempest_fixtures as fixtures
from tempest.common import utils
from tempest.lib.common.utils import data_utils
@@ -24,6 +25,8 @@
class QuotaClassesRbacTest(rbac_base.BaseV2ComputeRbacTest):
+ credentials = ['primary', 'admin']
+
def setUp(self):
# All test cases in this class need to externally lock on doing
# anything with default quota values.
@@ -48,11 +51,14 @@
def resource_setup(cls):
super(QuotaClassesRbacTest, cls).resource_setup()
# Create a project with its own quota.
- project_name = data_utils.rand_name(cls.__name__ + '-Project')
- cls.project_id = cls.identity_projects_client.create_project(
- project_name)['project']['id']
+ project_name = data_utils.rand_name(cls.__name__ + '-project')
+ project_desc = project_name + '-desc'
+ project = identity.identity_utils(cls.os_admin).create_project(
+ name=project_name, description=project_desc)
+ cls.project_id = project['id']
cls.addClassResourceCleanup(
- cls.identity_projects_client.delete_project, cls.project_id)
+ identity.identity_utils(cls.os_admin).delete_project,
+ cls.project_id)
@decorators.idempotent_id('c10198ed-9df2-440e-a49b-367dadc6de94')
@rbac_rule_validation.action(
diff --git a/patrole_tempest_plugin/tests/api/compute/test_quota_sets_rbac.py b/patrole_tempest_plugin/tests/api/compute/test_quota_sets_rbac.py
index 10734cb..2b05408 100644
--- a/patrole_tempest_plugin/tests/api/compute/test_quota_sets_rbac.py
+++ b/patrole_tempest_plugin/tests/api/compute/test_quota_sets_rbac.py
@@ -13,9 +13,9 @@
# License for the specific language governing permissions and limitations
# under the License.
+from tempest.common import identity
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
@@ -24,6 +24,8 @@
class QuotaSetsRbacTest(rbac_base.BaseV2ComputeRbacTest):
+ credentials = ['primary', 'admin']
+
@classmethod
def setup_clients(cls):
super(QuotaSetsRbacTest, cls).setup_clients()
@@ -89,10 +91,13 @@
def test_delete_quota_set(self):
project_name = data_utils.rand_name(
self.__class__.__name__ + '-project')
- project = self.projects_client.create_project(name=project_name)
- project_id = project['project']['id']
- self.addCleanup(test_utils.call_and_ignore_notfound_exc,
- self.projects_client.delete_project, project_id)
+ project_desc = project_name + '-desc'
+ project = identity.identity_utils(self.os_admin).create_project(
+ name=project_name, description=project_desc)
+ project_id = project['id']
+ self.addCleanup(
+ identity.identity_utils(self.os_admin).delete_project,
+ project_id)
with self.rbac_utils.override_role(self):
self.quotas_client.delete_quota_set(project_id)
diff --git a/patrole_tempest_plugin/tests/api/compute/test_server_actions_rbac.py b/patrole_tempest_plugin/tests/api/compute/test_server_actions_rbac.py
index a64bd20..0ff6ebe 100644
--- a/patrole_tempest_plugin/tests/api/compute/test_server_actions_rbac.py
+++ b/patrole_tempest_plugin/tests/api/compute/test_server_actions_rbac.py
@@ -403,5 +403,5 @@
server = self.servers_client.show_server(self.server_id)['server']
if 'host_status' not in server:
- raise rbac_exceptions.RbacMalformedResponse(
+ raise rbac_exceptions.RbacMissingAttributeResponseBody(
attribute='host_status')
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 88bea25..64e1300 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
@@ -143,7 +143,7 @@
expected_attr = 'config_drive'
# If the first server contains "config_drive", then all the others do.
if expected_attr not in body[0]:
- raise rbac_exceptions.RbacMalformedResponse(
+ raise rbac_exceptions.RbacMissingAttributeResponseBody(
attribute=expected_attr)
@testtools.skipIf(CONF.policy_feature_enabled.removed_nova_policies_stein,
@@ -159,7 +159,7 @@
body = self.servers_client.show_server(self.server['id'])['server']
expected_attr = 'config_drive'
if expected_attr not in body:
- raise rbac_exceptions.RbacMalformedResponse(
+ raise rbac_exceptions.RbacMissingAttributeResponseBody(
attribute=expected_attr)
@utils.requires_ext(extension='os-deferred-delete', service='compute')
@@ -188,7 +188,7 @@
body = self.servers_client.list_servers(detail=True)['servers']
# If the first server contains `expected_attr`, then all the others do.
if expected_attr not in body[0]:
- raise rbac_exceptions.RbacMalformedResponse(
+ raise rbac_exceptions.RbacMissingAttributeResponseBody(
attribute=expected_attr)
@testtools.skipIf(CONF.policy_feature_enabled.removed_nova_policies_stein,
@@ -205,7 +205,7 @@
with self.rbac_utils.override_role(self):
body = self.servers_client.show_server(self.server['id'])['server']
if expected_attr not in body:
- raise rbac_exceptions.RbacMalformedResponse(
+ raise rbac_exceptions.RbacMissingAttributeResponseBody(
attribute=expected_attr)
@testtools.skipIf(CONF.policy_feature_enabled.removed_nova_policies_stein,
@@ -229,7 +229,7 @@
for attr in ('host', 'instance_name'):
whole_attr = 'OS-EXT-SRV-ATTR:%s' % attr
if whole_attr not in body[0]:
- raise rbac_exceptions.RbacMalformedResponse(
+ raise rbac_exceptions.RbacMissingAttributeResponseBody(
attribute=whole_attr)
@testtools.skipIf(CONF.policy_feature_enabled.removed_nova_policies_stein,
@@ -253,7 +253,7 @@
for attr in ('host', 'instance_name'):
whole_attr = 'OS-EXT-SRV-ATTR:%s' % attr
if whole_attr not in body:
- raise rbac_exceptions.RbacMalformedResponse(
+ raise rbac_exceptions.RbacMissingAttributeResponseBody(
attribute=whole_attr)
@testtools.skipIf(CONF.policy_feature_enabled.removed_nova_policies_stein,
@@ -272,7 +272,7 @@
'OS-EXT-STS:power_state')
for attr in expected_attrs:
if attr not in body[0]:
- raise rbac_exceptions.RbacMalformedResponse(
+ raise rbac_exceptions.RbacMissingAttributeResponseBody(
attribute=attr)
@testtools.skipIf(CONF.policy_feature_enabled.removed_nova_policies_stein,
@@ -291,7 +291,7 @@
'OS-EXT-STS:power_state')
for attr in expected_attrs:
if attr not in body:
- raise rbac_exceptions.RbacMalformedResponse(
+ raise rbac_exceptions.RbacMissingAttributeResponseBody(
attribute=attr)
@testtools.skipIf(CONF.policy_feature_enabled.removed_nova_policies_stein,
@@ -310,7 +310,7 @@
with self.rbac_utils.override_role(self):
body = self.servers_client.list_servers(detail=True)['servers']
if expected_attr not in body[0]:
- raise rbac_exceptions.RbacMalformedResponse(
+ raise rbac_exceptions.RbacMissingAttributeResponseBody(
attribute=expected_attr)
@testtools.skipIf(CONF.policy_feature_enabled.removed_nova_policies_stein,
@@ -329,7 +329,7 @@
with self.rbac_utils.override_role(self):
body = self.servers_client.show_server(self.server['id'])['server']
if expected_attr not in body:
- raise rbac_exceptions.RbacMalformedResponse(
+ raise rbac_exceptions.RbacMissingAttributeResponseBody(
attribute=expected_attr)
@utils.requires_ext(extension='os-instance-actions', service='compute')
@@ -360,12 +360,12 @@
self.server['id'], request_id)['instanceAction']
if 'events' not in instance_action:
- raise rbac_exceptions.RbacMalformedResponse(
+ raise rbac_exceptions.RbacMissingAttributeResponseBody(
attribute='events')
# Microversion 2.51+ returns 'events' always, but not 'traceback'. If
# 'traceback' is also present then policy enforcement passed.
if 'traceback' not in instance_action['events'][0]:
- raise rbac_exceptions.RbacMalformedResponse(
+ raise rbac_exceptions.RbacMissingAttributeResponseBody(
attribute='events.traceback')
@testtools.skipIf(CONF.policy_feature_enabled.removed_nova_policies_stein,
@@ -379,7 +379,7 @@
result = self.servers_client.show_server(self.server['id'])[
'server']
if 'key_name' not in result:
- raise rbac_exceptions.RbacMalformedResponse(
+ raise rbac_exceptions.RbacMissingAttributeResponseBody(
attribute='key_name')
@testtools.skipIf(CONF.policy_feature_enabled.removed_nova_policies_stein,
@@ -392,7 +392,7 @@
with self.rbac_utils.override_role(self):
result = self.servers_client.list_servers(detail=True)['servers']
if 'key_name' not in result[0]:
- raise rbac_exceptions.RbacMalformedResponse(
+ raise rbac_exceptions.RbacMissingAttributeResponseBody(
attribute='key_name')
@rbac_rule_validation.action(
@@ -514,7 +514,7 @@
body = self.servers_client.show_server(self.server['id'])['server']
for expected_attr in expected_attrs:
if expected_attr not in body:
- raise rbac_exceptions.RbacMalformedResponse(
+ raise rbac_exceptions.RbacMissingAttributeResponseBody(
attribute=expected_attr)
@utils.requires_ext(extension='os-simple-tenant-usage', service='compute')
diff --git a/patrole_tempest_plugin/tests/api/identity/v3/test_credentials_rbac.py b/patrole_tempest_plugin/tests/api/identity/v3/test_credentials_rbac.py
index 977a830..26e34da 100644
--- a/patrole_tempest_plugin/tests/api/identity/v3/test_credentials_rbac.py
+++ b/patrole_tempest_plugin/tests/api/identity/v3/test_credentials_rbac.py
@@ -13,13 +13,21 @@
# License for the specific language governing permissions and limitations
# under the License.
+import testtools
+
+from tempest import config
from tempest.lib.common.utils import data_utils
from tempest.lib import decorators
from patrole_tempest_plugin import rbac_rule_validation
from patrole_tempest_plugin.tests.api.identity import rbac_base
+CONF = config.CONF
+
+@testtools.skipIf(
+ CONF.policy_feature_enabled.removed_keystone_policies_stein,
+ "This policy is unavailable in Stein so cannot be tested.")
class IdentityCredentialsV3RbacTest(rbac_base.BaseIdentityV3RbacTest):
def _create_user_project_and_credential(self):
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
index cf73669..893942e 100644
--- a/patrole_tempest_plugin/tests/api/network/test_address_scope_rbac.py
+++ b/patrole_tempest_plugin/tests/api/network/test_address_scope_rbac.py
@@ -23,18 +23,18 @@
from patrole_tempest_plugin.tests.api.network import rbac_base as base
-class AddressScopeRbacTest(base.BaseNetworkPluginRbacTest):
+class AddressScopePluginRbacTest(base.BaseNetworkPluginRbacTest):
@classmethod
def skip_checks(cls):
- super(AddressScopeRbacTest, cls).skip_checks()
+ super(AddressScopePluginRbacTest, 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()
+ super(AddressScopePluginRbacTest, cls).resource_setup()
cls.network = cls.create_network()
def _create_address_scope(self, name=None, **kwargs):
@@ -121,7 +121,7 @@
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)
+ shared=True)
@rbac_rule_validation.action(service="neutron",
rules=["get_address_scope",
diff --git a/patrole_tempest_plugin/tests/api/network/test_auto_allocated_topology_rbac.py b/patrole_tempest_plugin/tests/api/network/test_auto_allocated_topology_rbac.py
index bcf62d7..7098e55 100644
--- a/patrole_tempest_plugin/tests/api/network/test_auto_allocated_topology_rbac.py
+++ b/patrole_tempest_plugin/tests/api/network/test_auto_allocated_topology_rbac.py
@@ -35,10 +35,44 @@
rules=["get_auto_allocated_topology"],
expected_error_codes=[404])
def test_show_auto_allocated_topology(self):
- """Show auto_allocated_topology.
+ """Test show auto_allocated_topology.
RBAC test for the neutron "get_auto_allocated_topology" policy
"""
with self.rbac_utils.override_role(self):
self.ntp_client.get_auto_allocated_topology(
tenant_id=self.os_primary.credentials.tenant_id)
+
+ def _ensure_network_not_in_use(cls, network_id):
+ ports = cls.ntp_client.list_ports(network_id=network_id)["ports"]
+
+ # Every subnet within network should have a router interface
+ expected_ports_count = len(
+ cls.ntp_client.show_network(network_id)["network"]["subnets"])
+ # Every network should have a single dhcp interface
+ expected_ports_count += 1
+
+ if len(ports) != expected_ports_count:
+ msg = "Auto Allocated Topology in use."
+ cls.skipException(msg)
+
+ @decorators.idempotent_id('A0606AFE-065E-4C09-8E51-58EE7FBA30A2')
+ @decorators.attr(type='slow')
+ @rbac_rule_validation.action(service="neutron",
+ rules=["get_auto_allocated_topology",
+ "delete_auto_allocated_topology"],
+ expected_error_codes=[404, 403])
+ def test_delete_auto_allocated_topology(self):
+ """Test delete auto_allocated_topology.
+
+ RBAC test for the neutron "delete_auto_allocated_topology" policy
+ """
+ tenant_id = self.os_primary.credentials.tenant_id
+ net_id = self.ntp_client.get_auto_allocated_topology(
+ tenant_id=tenant_id)["auto_allocated_topology"]["id"]
+
+ self._ensure_network_not_in_use(net_id)
+
+ with self.rbac_utils.override_role(self):
+ self.ntp_client.delete_auto_allocated_topology(
+ tenant_id=self.os_primary.credentials.tenant_id)
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 c985111..b449970 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
@@ -118,4 +118,4 @@
LOG.info("NotFound or Forbidden exception are not thrown when "
"role doesn't have access to the endpoint. Instead, "
"the response will have an empty network body.")
- raise rbac_exceptions.RbacMalformedResponse(empty=True)
+ raise rbac_exceptions.RbacEmptyResponseBody()
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 96ba378..b39489a 100644
--- a/patrole_tempest_plugin/tests/api/network/test_networks_rbac.py
+++ b/patrole_tempest_plugin/tests/api/network/test_networks_rbac.py
@@ -363,7 +363,7 @@
self.network['id'], **kwargs)['network']
if len(retrieved_network) == 0:
- raise rbac_exceptions.RbacMalformedResponse(empty=True)
+ raise rbac_exceptions.RbacEmptyResponseBody()
@utils.requires_ext(extension='provider', service='network')
@rbac_rule_validation.action(service="neutron",
@@ -384,7 +384,7 @@
self.network['id'], **kwargs)['network']
if len(retrieved_network) == 0:
- raise rbac_exceptions.RbacMalformedResponse(empty=True)
+ raise rbac_exceptions.RbacEmptyResponseBody()
@utils.requires_ext(extension='provider', service='network')
@rbac_rule_validation.action(
@@ -406,7 +406,7 @@
self.network['id'], **kwargs)['network']
if len(retrieved_network) == 0:
- raise rbac_exceptions.RbacMalformedResponse(empty=True)
+ raise rbac_exceptions.RbacEmptyResponseBody()
@utils.requires_ext(extension='provider', service='network')
@rbac_rule_validation.action(
@@ -428,7 +428,7 @@
self.network['id'], **kwargs)['network']
if len(retrieved_network) == 0:
- raise rbac_exceptions.RbacMalformedResponse(empty=True)
+ raise rbac_exceptions.RbacEmptyResponseBody()
@rbac_rule_validation.action(service="neutron",
rules=["get_network", "delete_network"],
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 b65bd73..dd3537f 100644
--- a/patrole_tempest_plugin/tests/api/network/test_ports_rbac.py
+++ b/patrole_tempest_plugin/tests/api/network/test_ports_rbac.py
@@ -183,7 +183,7 @@
# Rather than throwing a 403, the field is not present, so raise exc.
if fields[0] not in retrieved_port:
- raise rbac_exceptions.RbacMalformedResponse(
+ raise rbac_exceptions.RbacMissingAttributeResponseBody(
attribute='binding:vif_type')
@utils.requires_ext(extension='binding', service='network')
@@ -203,7 +203,7 @@
# Rather than throwing a 403, the field is not present, so raise exc.
if fields[0] not in retrieved_port:
- raise rbac_exceptions.RbacMalformedResponse(
+ raise rbac_exceptions.RbacMissingAttributeResponseBody(
attribute='binding:vif_details')
@utils.requires_ext(extension='binding', service='network')
@@ -226,7 +226,7 @@
# Rather than throwing a 403, the field is not present, so raise exc.
if fields[0] not in retrieved_port:
- raise rbac_exceptions.RbacMalformedResponse(
+ raise rbac_exceptions.RbacMissingAttributeResponseBody(
attribute='binding:host_id')
@utils.requires_ext(extension='binding', service='network')
@@ -250,7 +250,7 @@
# Rather than throwing a 403, the field is not present, so raise exc.
if fields[0] not in retrieved_port:
- raise rbac_exceptions.RbacMalformedResponse(
+ raise rbac_exceptions.RbacMissingAttributeResponseBody(
attribute='binding:profile')
@rbac_rule_validation.action(service="neutron",
diff --git a/patrole_tempest_plugin/tests/api/network/test_qos_rbac.py b/patrole_tempest_plugin/tests/api/network/test_qos_rbac.py
index 20f9e61..aae326c 100644
--- a/patrole_tempest_plugin/tests/api/network/test_qos_rbac.py
+++ b/patrole_tempest_plugin/tests/api/network/test_qos_rbac.py
@@ -22,18 +22,18 @@
from patrole_tempest_plugin.tests.api.network import rbac_base as base
-class QosRbacTest(base.BaseNetworkPluginRbacTest):
+class QosPluginRbacTest(base.BaseNetworkPluginRbacTest):
@classmethod
def skip_checks(cls):
- super(QosRbacTest, cls).skip_checks()
+ super(QosPluginRbacTest, 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()
+ super(QosPluginRbacTest, cls).resource_setup()
cls.network = cls.create_network()
def create_policy(self, name=None):
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 f850a3e..399ad47 100644
--- a/patrole_tempest_plugin/tests/api/network/test_routers_rbac.py
+++ b/patrole_tempest_plugin/tests/api/network/test_routers_rbac.py
@@ -179,7 +179,7 @@
# Rather than throwing a 403, the field is not present, so raise exc.
if 'distributed' not in retrieved_fields:
- raise rbac_exceptions.RbacMalformedResponse(
+ raise rbac_exceptions.RbacMissingAttributeResponseBody(
attribute='distributed')
@decorators.idempotent_id('defc502c-4159-4824-b4d9-3cdcc39015b2')
@@ -201,7 +201,7 @@
# Rather than throwing a 403, the field is not present, so raise exc.
if 'ha' not in retrieved_fields:
- raise rbac_exceptions.RbacMalformedResponse(
+ raise rbac_exceptions.RbacMissingAttributeResponseBody(
attribute='ha')
@rbac_rule_validation.action(service="neutron",
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 9112bf6..e9fa018 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
@@ -126,7 +126,7 @@
# Neutron may return an empty list if access is denied.
if not security_groups['security_groups']:
- raise rbac_exceptions.RbacMalformedResponse(empty=True)
+ raise rbac_exceptions.RbacEmptyResponseBody()
@rbac_rule_validation.action(service="neutron",
rules=["create_security_group_rule"])
@@ -170,4 +170,4 @@
# Neutron may return an empty list if access is denied.
if not security_rules['security_group_rules']:
- raise rbac_exceptions.RbacMalformedResponse(empty=True)
+ raise rbac_exceptions.RbacEmptyResponseBody()
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 9a5ebe4..8fe157a 100644
--- a/patrole_tempest_plugin/tests/api/network/test_subnets_rbac.py
+++ b/patrole_tempest_plugin/tests/api/network/test_subnets_rbac.py
@@ -73,7 +73,7 @@
# Neutron may return an empty list if access is denied.
if not subnets['subnets']:
- raise rbac_exceptions.RbacMalformedResponse(empty=True)
+ raise rbac_exceptions.RbacEmptyResponseBody()
@decorators.idempotent_id('f36cd821-dd22-4bd0-b43d-110fc4b553eb')
@rbac_rule_validation.action(service="neutron",
diff --git a/patrole_tempest_plugin/tests/api/volume/rbac_base.py b/patrole_tempest_plugin/tests/api/volume/rbac_base.py
index 14b3151..1d0a44d 100644
--- a/patrole_tempest_plugin/tests/api/volume/rbac_base.py
+++ b/patrole_tempest_plugin/tests/api/volume/rbac_base.py
@@ -32,10 +32,10 @@
def setup_clients(cls):
super(BaseVolumeRbacTest, cls).setup_clients()
cls.setup_rbac_utils()
- cls.volume_hosts_client = cls.os_primary.volume_hosts_v2_client
- cls.volume_types_client = cls.os_primary.volume_types_v2_client
- cls.groups_client = cls.os_primary.groups_v3_client
- cls.group_types_client = cls.os_primary.group_types_v3_client
+ cls.volume_hosts_client = cls.os_primary.volume_hosts_client_latest
+ cls.volume_types_client = cls.os_primary.volume_types_client_latest
+ cls.groups_client = cls.os_primary.groups_client_latest
+ cls.group_types_client = cls.os_primary.group_types_client_latest
@classmethod
def create_volume_type(cls, name=None, **kwargs):
diff --git a/patrole_tempest_plugin/tests/api/volume/test_capabilities_rbac.py b/patrole_tempest_plugin/tests/api/volume/test_capabilities_rbac.py
index fa1157a..3770f84 100644
--- a/patrole_tempest_plugin/tests/api/volume/test_capabilities_rbac.py
+++ b/patrole_tempest_plugin/tests/api/volume/test_capabilities_rbac.py
@@ -32,8 +32,9 @@
@classmethod
def setup_clients(cls):
super(CapabilitiesV3RbacTest, cls).setup_clients()
- cls.capabilities_client = cls.os_primary.volume_capabilities_v2_client
- cls.hosts_client = cls.os_primary.volume_hosts_v2_client
+ cls.capabilities_client = \
+ cls.os_primary.volume_capabilities_client_latest
+ cls.hosts_client = cls.os_primary.volume_hosts_client_latest
@rbac_rule_validation.action(service="cinder",
rules=["volume_extension:capabilities"])
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 0eb0244..8443943 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
@@ -56,7 +56,8 @@
@classmethod
def setup_clients(cls):
super(EncryptionTypesV3RbacTest, cls).setup_clients()
- cls.encryption_types_client = cls.os_primary.encryption_types_v2_client
+ cls.encryption_types_client = \
+ cls.os_primary.encryption_types_client_latest
def _create_volume_type_encryption(self):
vol_type_id = self.create_volume_type()['id']
diff --git a/patrole_tempest_plugin/tests/api/volume/test_group_snapshots_rbac.py b/patrole_tempest_plugin/tests/api/volume/test_group_snapshots_rbac.py
index 1d59f9b..56a0233 100644
--- a/patrole_tempest_plugin/tests/api/volume/test_group_snapshots_rbac.py
+++ b/patrole_tempest_plugin/tests/api/volume/test_group_snapshots_rbac.py
@@ -75,7 +75,7 @@
def setup_clients(cls):
super(GroupSnaphotsV314RbacTest, cls).setup_clients()
cls.group_snapshot_client = \
- cls.os_primary.group_snapshots_v3_client
+ cls.os_primary.group_snapshots_client_latest
def setUp(self):
super(GroupSnaphotsV314RbacTest, self).setUp()
@@ -172,7 +172,7 @@
def setup_clients(cls):
super(GroupSnaphotsV319RbacTest, cls).setup_clients()
cls.group_snapshot_client = \
- cls.os_primary.group_snapshots_v3_client
+ cls.os_primary.group_snapshots_client_latest
def setUp(self):
super(GroupSnaphotsV319RbacTest, self).setUp()
diff --git a/patrole_tempest_plugin/tests/api/volume/test_groups_rbac.py b/patrole_tempest_plugin/tests/api/volume/test_groups_rbac.py
index c117d23..730e349 100644
--- a/patrole_tempest_plugin/tests/api/volume/test_groups_rbac.py
+++ b/patrole_tempest_plugin/tests/api/volume/test_groups_rbac.py
@@ -208,7 +208,7 @@
group_type = self.create_group_type(ignore_notfound=True)
if 'group_specs' not in group_type:
- raise rbac_exceptions.RbacMalformedResponse(
+ raise rbac_exceptions.RbacMissingAttributeResponseBody(
attribute='group_specs')
@decorators.idempotent_id('8d9e2831-24c3-47b7-a76a-2e563287f12f')
@@ -221,5 +221,5 @@
resp_body = self.group_types_client.show_group_type(
group_type['id'])['group_type']
if 'group_specs' not in resp_body:
- raise rbac_exceptions.RbacMalformedResponse(
+ raise rbac_exceptions.RbacMissingAttributeResponseBody(
attribute='group_specs')
diff --git a/patrole_tempest_plugin/tests/api/volume/test_limits_rbac.py b/patrole_tempest_plugin/tests/api/volume/test_limits_rbac.py
index 3127d83..2bd0992 100644
--- a/patrole_tempest_plugin/tests/api/volume/test_limits_rbac.py
+++ b/patrole_tempest_plugin/tests/api/volume/test_limits_rbac.py
@@ -51,4 +51,5 @@
'limits']['absolute']
for key in expected_keys:
if key not in absolute_limits:
- raise rbac_exceptions.RbacMalformedResponse(attribute=key)
+ raise rbac_exceptions.RbacMissingAttributeResponseBody(
+ attribute=key)
diff --git a/patrole_tempest_plugin/tests/api/volume/test_qos_rbac.py b/patrole_tempest_plugin/tests/api/volume/test_qos_rbac.py
index 5664bf9..2f144b0 100644
--- a/patrole_tempest_plugin/tests/api/volume/test_qos_rbac.py
+++ b/patrole_tempest_plugin/tests/api/volume/test_qos_rbac.py
@@ -27,7 +27,7 @@
@classmethod
def setup_clients(cls):
super(VolumeQOSV3RbacTest, cls).setup_clients()
- cls.qos_client = cls.os_primary.volume_qos_v2_client
+ cls.qos_client = cls.os_primary.volume_qos_client_latest
def _create_test_qos_specs(self, name=None, consumer=None, **kwargs):
name = name or data_utils.rand_name(self.__class__.__name__ + '-QoS')
diff --git a/patrole_tempest_plugin/tests/api/volume/test_scheduler_stats_rbac.py b/patrole_tempest_plugin/tests/api/volume/test_scheduler_stats_rbac.py
index efcfdaf..ff12cba 100644
--- a/patrole_tempest_plugin/tests/api/volume/test_scheduler_stats_rbac.py
+++ b/patrole_tempest_plugin/tests/api/volume/test_scheduler_stats_rbac.py
@@ -33,7 +33,7 @@
def setup_clients(cls):
super(SchedulerStatsV3RbacTest, cls).setup_clients()
cls.scheduler_stats_client =\
- cls.os_primary.volume_scheduler_stats_v2_client
+ cls.os_primary.volume_scheduler_stats_client_latest
@rbac_rule_validation.action(
service="cinder",
diff --git a/patrole_tempest_plugin/tests/api/volume/test_snapshot_manage_rbac.py b/patrole_tempest_plugin/tests/api/volume/test_snapshot_manage_rbac.py
index e2887e0..1fc4c24 100644
--- a/patrole_tempest_plugin/tests/api/volume/test_snapshot_manage_rbac.py
+++ b/patrole_tempest_plugin/tests/api/volume/test_snapshot_manage_rbac.py
@@ -40,7 +40,8 @@
@classmethod
def setup_clients(cls):
super(SnapshotManageRbacTest, cls).setup_clients()
- cls.snapshot_manage_client = cls.os_primary.snapshot_manage_v2_client
+ cls.snapshot_manage_client = \
+ cls.os_primary.snapshot_manage_client_latest
@classmethod
def resource_setup(cls):
diff --git a/patrole_tempest_plugin/tests/api/volume/test_user_messages_rbac.py b/patrole_tempest_plugin/tests/api/volume/test_user_messages_rbac.py
index 962a9b1..11c42b1 100644
--- a/patrole_tempest_plugin/tests/api/volume/test_user_messages_rbac.py
+++ b/patrole_tempest_plugin/tests/api/volume/test_user_messages_rbac.py
@@ -31,7 +31,7 @@
@classmethod
def setup_clients(cls):
super(MessagesV3RbacTest, cls).setup_clients()
- cls.messages_client = cls.os_primary.volume_v3_messages_client
+ cls.messages_client = cls.os_primary.volume_messages_client_latest
def _create_user_message(self):
"""Trigger a 'no valid host' situation to generate a message."""
diff --git a/patrole_tempest_plugin/tests/api/volume/test_volume_metadata_rbac.py b/patrole_tempest_plugin/tests/api/volume/test_volume_metadata_rbac.py
index 6c2c84d..7e0044d 100644
--- a/patrole_tempest_plugin/tests/api/volume/test_volume_metadata_rbac.py
+++ b/patrole_tempest_plugin/tests/api/volume/test_volume_metadata_rbac.py
@@ -111,7 +111,7 @@
'volumes']
expected_attr = 'volume_image_metadata'
if expected_attr not in resp_body[0]:
- raise rbac_exceptions.RbacMalformedResponse(
+ raise rbac_exceptions.RbacMissingAttributeResponseBody(
attribute=expected_attr)
@decorators.idempotent_id('53f94d52-0dd5-42cf-a3a4-59b35150b3d5')
@@ -129,7 +129,7 @@
'volume']
expected_attr = 'volume_image_metadata'
if expected_attr not in resp_body:
- raise rbac_exceptions.RbacMalformedResponse(
+ raise rbac_exceptions.RbacMissingAttributeResponseBody(
attribute=expected_attr)
@decorators.idempotent_id('a9d9e825-5ea3-42e6-96f3-7ac4e97b2ed0')
diff --git a/patrole_tempest_plugin/tests/api/volume/test_volume_quotas_rbac.py b/patrole_tempest_plugin/tests/api/volume/test_volume_quotas_rbac.py
index cd1fb6e..f49c2fd 100644
--- a/patrole_tempest_plugin/tests/api/volume/test_volume_quotas_rbac.py
+++ b/patrole_tempest_plugin/tests/api/volume/test_volume_quotas_rbac.py
@@ -32,7 +32,7 @@
@classmethod
def setup_clients(cls):
super(VolumeQuotasV3RbacTest, cls).setup_clients()
- cls.quotas_client = cls.os_primary.volume_quotas_v2_client
+ cls.quotas_client = cls.os_primary.volume_quotas_client_latest
def _restore_default_quota_set(self):
default_quota_set = self.quotas_client.show_default_quota_set(
diff --git a/patrole_tempest_plugin/tests/api/volume/test_volume_services_rbac.py b/patrole_tempest_plugin/tests/api/volume/test_volume_services_rbac.py
index 9f97a82..0f4e458 100644
--- a/patrole_tempest_plugin/tests/api/volume/test_volume_services_rbac.py
+++ b/patrole_tempest_plugin/tests/api/volume/test_volume_services_rbac.py
@@ -36,7 +36,7 @@
@classmethod
def setup_clients(cls):
super(VolumeServicesV3RbacTest, cls).setup_clients()
- cls.services_client = cls.os_primary.volume_services_v2_client
+ cls.services_client = cls.os_primary.volume_services_client_latest
@decorators.idempotent_id('b9134f01-97c0-4abd-9455-fe2f03e3f966')
@rbac_rule_validation.action(
diff --git a/patrole_tempest_plugin/tests/api/volume/test_volume_transfers_rbac.py b/patrole_tempest_plugin/tests/api/volume/test_volume_transfers_rbac.py
index a18a370..5e0fd21 100644
--- a/patrole_tempest_plugin/tests/api/volume/test_volume_transfers_rbac.py
+++ b/patrole_tempest_plugin/tests/api/volume/test_volume_transfers_rbac.py
@@ -26,7 +26,7 @@
@classmethod
def setup_clients(cls):
super(VolumesTransfersV3RbacTest, cls).setup_clients()
- cls.transfers_client = cls.os_primary.volume_transfers_v2_client
+ cls.transfers_client = cls.os_primary.volume_transfers_client_latest
@classmethod
def resource_setup(cls):
@@ -54,12 +54,17 @@
def test_create_volume_transfer(self):
with self.rbac_utils.override_role(self):
self._create_transfer()
+ waiters.wait_for_volume_resource_status(
+ self.volumes_client, self.volume['id'], 'awaiting-transfer')
@rbac_rule_validation.action(service="cinder",
rules=["volume:get_transfer"])
@decorators.idempotent_id('7a0925d3-ed97-4c25-8299-e5cdabe2eb55')
def test_get_volume_transfer(self):
transfer = self._create_transfer()
+ waiters.wait_for_volume_resource_status(
+ self.volumes_client, self.volume['id'], 'awaiting-transfer')
+
with self.rbac_utils.override_role(self):
self.transfers_client.show_volume_transfer(transfer['id'])
@@ -82,15 +87,23 @@
@decorators.idempotent_id('987f2a11-d657-4984-a6c9-28f06c1cd014')
def test_accept_volume_transfer(self):
transfer = self._create_transfer()
+ waiters.wait_for_volume_resource_status(
+ self.volumes_client, self.volume['id'], 'awaiting-transfer')
+
with self.rbac_utils.override_role(self):
self.transfers_client.accept_volume_transfer(
transfer['id'], auth_key=transfer['auth_key'])
+ waiters.wait_for_volume_resource_status(self.volumes_client,
+ self.volume['id'], 'available')
@rbac_rule_validation.action(service="cinder",
rules=["volume:delete_transfer"])
@decorators.idempotent_id('4672187e-7fff-454b-832a-5c8865dda868')
def test_delete_volume_transfer(self):
transfer = self._create_transfer()
+ waiters.wait_for_volume_resource_status(
+ self.volumes_client, self.volume['id'], 'awaiting-transfer')
+
with self.rbac_utils.override_role(self):
self.transfers_client.delete_volume_transfer(transfer['id'])
waiters.wait_for_volume_resource_status(
diff --git a/patrole_tempest_plugin/tests/api/volume/test_volumes_backup_rbac.py b/patrole_tempest_plugin/tests/api/volume/test_volumes_backup_rbac.py
index bf22341..0efeb33 100644
--- a/patrole_tempest_plugin/tests/api/volume/test_volumes_backup_rbac.py
+++ b/patrole_tempest_plugin/tests/api/volume/test_volumes_backup_rbac.py
@@ -210,7 +210,7 @@
# Show backup API attempts to inject the attribute below into the
# response body but only if policy enforcement succeeds.
if self.expected_attr not in body:
- raise rbac_exceptions.RbacMalformedResponse(
+ raise rbac_exceptions.RbacMissingAttributeResponseBody(
attribute=self.expected_attr)
@decorators.idempotent_id('aa40b7c0-5974-48be-8cbc-e23cc61c4c68')
@@ -221,7 +221,7 @@
body = self.backups_client.list_backups(detail=True)['backups']
if self.expected_attr not in body[0]:
- raise rbac_exceptions.RbacMalformedResponse(
+ raise rbac_exceptions.RbacMissingAttributeResponseBody(
attribute=self.expected_attr)
diff --git a/patrole_tempest_plugin/tests/api/volume/test_volumes_manage_rbac.py b/patrole_tempest_plugin/tests/api/volume/test_volumes_manage_rbac.py
index 2782e22..9f21c4a 100644
--- a/patrole_tempest_plugin/tests/api/volume/test_volumes_manage_rbac.py
+++ b/patrole_tempest_plugin/tests/api/volume/test_volumes_manage_rbac.py
@@ -41,7 +41,7 @@
@classmethod
def setup_clients(cls):
super(VolumesManageV3RbacTest, cls).setup_clients()
- cls.volume_manage_client = cls.os_primary.volume_manage_v2_client
+ cls.volume_manage_client = cls.os_primary.volume_manage_client_latest
def _manage_volume(self, org_volume):
# Manage volume
diff --git a/patrole_tempest_plugin/tests/api/volume/test_volumes_snapshots_rbac.py b/patrole_tempest_plugin/tests/api/volume/test_volumes_snapshots_rbac.py
index 40469a2..55adf1a 100644
--- a/patrole_tempest_plugin/tests/api/volume/test_volumes_snapshots_rbac.py
+++ b/patrole_tempest_plugin/tests/api/volume/test_volumes_snapshots_rbac.py
@@ -76,7 +76,7 @@
self.snapshot['id'])['snapshot']
for expected_attr in expected_attrs:
if expected_attr not in resp:
- raise rbac_exceptions.RbacMalformedResponse(
+ raise rbac_exceptions.RbacMissingAttributeResponseBody(
attribute=expected_attr)
@rbac_rule_validation.action(service="cinder",
@@ -136,5 +136,5 @@
resp = self._list_by_param_values(with_detail=True, **params)
for expected_attr in expected_attrs:
if expected_attr not in resp[0]:
- raise rbac_exceptions.RbacMalformedResponse(
+ raise rbac_exceptions.RbacMissingAttributeResponseBody(
attribute=expected_attr)
diff --git a/patrole_tempest_plugin/tests/unit/fixtures.py b/patrole_tempest_plugin/tests/unit/fixtures.py
index 4552224..78e87e5 100644
--- a/patrole_tempest_plugin/tests/unit/fixtures.py
+++ b/patrole_tempest_plugin/tests/unit/fixtures.py
@@ -66,15 +66,17 @@
def setUp(self):
super(RbacUtilsFixture, self).setUp()
- self.useFixture(ConfPatcher(rbac_test_role='member', group='patrole'))
+ self.useFixture(ConfPatcher(rbac_test_roles=['member'],
+ group='patrole'))
self.useFixture(ConfPatcher(
admin_role='admin', auth_version='v3', group='identity'))
+ self.useFixture(ConfPatcher(
+ api_v3=True, group='identity-feature-enabled'))
test_obj_kwargs = {
'os_primary.credentials.user_id': self.USER_ID,
'os_primary.credentials.tenant_id': self.PROJECT_ID,
'os_primary.credentials.project_id': self.PROJECT_ID,
- 'get_identity_version.return_value': 'v3'
}
self.mock_test_obj = mock.Mock(
__name__='patrole_unit_test', spec=test.BaseTestCase,
@@ -91,7 +93,7 @@
mock_admin_mgr = mock.patch.object(
clients, 'Manager', spec=clients.Manager,
roles_v3_client=mock.Mock(), roles_client=mock.Mock()).start()
- self.roles_v3_client = mock_admin_mgr.return_value.roles_v3_client
+ self.admin_roles_client = mock_admin_mgr.return_value.roles_v3_client
self.set_roles(['admin', 'member'], [])
@@ -152,6 +154,6 @@
for role in roles_on_project]
}
- self.roles_v3_client.list_roles.return_value = available_roles
- self.roles_v3_client.list_user_roles_on_project.return_value = (
+ self.admin_roles_client.list_roles.return_value = available_roles
+ self.admin_roles_client.list_user_roles_on_project.return_value = (
available_project_roles)
diff --git a/patrole_tempest_plugin/tests/unit/test_hacking.py b/patrole_tempest_plugin/tests/unit/test_hacking.py
index 6096c24..d35b816 100644
--- a/patrole_tempest_plugin/tests/unit/test_hacking.py
+++ b/patrole_tempest_plugin/tests/unit/test_hacking.py
@@ -256,3 +256,53 @@
self.assertTrue(checks.no_client_alias_in_test_cases(
" cls.client",
"./patrole_tempest_plugin/tests/api/fake_test_rbac.py"))
+
+ def test_no_plugin_rbac_test_suffix_in_plugin_test_class_name(self):
+ check = checks.no_plugin_rbac_test_suffix_in_plugin_test_class_name
+
+ # Passing cases: these do not inherit from "PluginRbacTest" base class.
+ self.assertFalse(check(
+ "class FakeRbacTest(BaseFakeRbacTest)",
+ "./patrole_tempest_plugin/tests/api/fake_test_rbac.py"))
+ self.assertFalse(check(
+ "class FakeRbacTest(base.BaseFakeRbacTest)",
+ "./patrole_tempest_plugin/tests/api/fake_test_rbac.py"))
+
+ # Passing cases: these **do** end in correct test class suffix.
+ self.assertFalse(check(
+ "class FakePluginRbacTest(BaseFakePluginRbacTest)",
+ "./patrole_tempest_plugin/tests/api/fake_test_rbac.py"))
+ self.assertFalse(check(
+ "class FakePluginRbacTest(base.BaseFakePluginRbacTest)",
+ "./patrole_tempest_plugin/tests/api/fake_test_rbac.py"))
+
+ # Passing cases: plugin base class inherits from another base class.
+ self.assertFalse(check(
+ "class BaseFakePluginRbacTest(base.BaseFakeRbacTest)",
+ "./patrole_tempest_plugin/tests/api/fake_test_rbac.py"))
+ self.assertFalse(check(
+ "class BaseFakePluginRbacTest(BaseFakeRbacTest)",
+ "./patrole_tempest_plugin/tests/api/fake_test_rbac.py"))
+
+ # Failing cases: these **do not** end in correct test class suffix.
+ # Case 1: RbacTest subclass doesn't end in PluginRbacTest.
+ self.assertTrue(check(
+ "class FakeRbacTest(base.BaseFakePluginRbacTest)",
+ "./patrole_tempest_plugin/tests/api/fake_test_rbac.py"))
+ self.assertTrue(check(
+ "class FakeRbacTest(BaseFakePluginRbacTest)",
+ "./patrole_tempest_plugin/tests/api/fake_test_rbac.py"))
+ self.assertTrue(check(
+ "class FakeRbacTest(BaseFakeNetworkPluginRbacTest)",
+ "./patrole_tempest_plugin/tests/api/network/fake_test_rbac.py"))
+ # Case 2: PluginRbacTest subclass doesn't inherit from
+ # BasePluginRbacTest.
+ self.assertTrue(check(
+ "class FakePluginRbacTest(base.BaseFakeRbacTest)",
+ "./patrole_tempest_plugin/tests/api/fake_test_rbac.py"))
+ self.assertTrue(check(
+ "class FakePluginRbacTest(BaseFakeRbacTest)",
+ "./patrole_tempest_plugin/tests/api/fake_test_rbac.py"))
+ self.assertTrue(check(
+ "class FakeNeutronPluginRbacTest(BaseFakeNeutronRbacTest)",
+ "./patrole_tempest_plugin/tests/api/fake_test_rbac.py"))
diff --git a/patrole_tempest_plugin/tests/unit/test_policy_authority.py b/patrole_tempest_plugin/tests/unit/test_policy_authority.py
index 624c0c5..6a4d219 100644
--- a/patrole_tempest_plugin/tests/unit/test_policy_authority.py
+++ b/patrole_tempest_plugin/tests/unit/test_policy_authority.py
@@ -108,9 +108,60 @@
for rule, role_list in expected.items():
for role in role_list:
- self.assertTrue(authority.allowed(rule, role))
+ self.assertTrue(authority.allowed(rule, [role]))
for role in set(default_roles) - set(role_list):
- self.assertFalse(authority.allowed(rule, role))
+ self.assertFalse(authority.allowed(rule, [role]))
+
+ @mock.patch.object(policy_authority, 'LOG', autospec=True)
+ def _test_custom_multi_roles_policy(self, *args):
+ default_roles = ['zero', 'one', 'two', 'three', 'four',
+ 'five', 'six', 'seven', 'eight', 'nine']
+
+ test_tenant_id = mock.sentinel.tenant_id
+ test_user_id = mock.sentinel.user_id
+ authority = policy_authority.PolicyAuthority(
+ test_tenant_id, test_user_id, "custom_rbac_policy")
+
+ expected = {
+ 'policy_action_1': ['two', 'four', 'six', 'eight'],
+ 'policy_action_2': ['one', 'three', 'five', 'seven', 'nine'],
+ 'policy_action_4': ['one', 'two', 'three', 'five', 'seven'],
+ 'policy_action_5': ['zero', 'one', 'two', 'three', 'four', 'five',
+ 'six', 'seven', 'eight', 'nine'],
+ }
+
+ for rule, role_list in expected.items():
+ allowed_roles_lists = [roles for roles in [
+ role_list[len(role_list) // 2:],
+ role_list[:len(role_list) // 2]] if roles]
+ for test_roles in allowed_roles_lists:
+ self.assertTrue(authority.allowed(rule, test_roles))
+
+ disallowed_roles = list(set(default_roles) - set(role_list))
+ disallowed_roles_lists = [roles for roles in [
+ disallowed_roles[len(disallowed_roles) // 2:],
+ disallowed_roles[:len(disallowed_roles) // 2]] if roles]
+ for test_roles in disallowed_roles_lists:
+ self.assertFalse(authority.allowed(rule, test_roles))
+
+ def test_empty_rbac_test_roles(self):
+ test_tenant_id = mock.sentinel.tenant_id
+ test_user_id = mock.sentinel.user_id
+ authority = policy_authority.PolicyAuthority(
+ test_tenant_id, test_user_id, "custom_rbac_policy")
+
+ disallowed_for_empty_roles = ['policy_action_1', 'policy_action_2',
+ 'policy_action_3', 'policy_action_4',
+ 'policy_action_6']
+
+ # Due to "policy_action_5": "rule:all_rule" / "all_rule": ""
+ allowed_for_empty_roles = ['policy_action_5']
+
+ for rule in disallowed_for_empty_roles:
+ self.assertFalse(authority.allowed(rule, []))
+
+ for rule in allowed_for_empty_roles:
+ self.assertTrue(authority.allowed(rule, []))
def test_custom_policy_json(self):
# The CONF.patrole.custom_policy_files has a path to JSON file by
@@ -122,24 +173,34 @@
custom_policy_files=[self.conf_policy_path_yaml], group='patrole'))
self._test_custom_policy()
+ def test_custom_multi_roles_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_multi_roles_policy()
+
+ def test_custom_multi_roles_policy_yaml(self):
+ self.useFixture(fixtures.ConfPatcher(
+ custom_policy_files=[self.conf_policy_path_yaml], group='patrole'))
+ self._test_custom_multi_roles_policy()
+
def test_admin_policy_file_with_admin_role(self):
test_tenant_id = mock.sentinel.tenant_id
test_user_id = mock.sentinel.user_id
authority = policy_authority.PolicyAuthority(
test_tenant_id, test_user_id, "admin_rbac_policy")
- role = 'admin'
+ roles = ['admin']
allowed_rules = [
'admin_rule', 'is_admin_rule', 'alt_admin_rule'
]
disallowed_rules = ['non_admin_rule']
for rule in allowed_rules:
- allowed = authority.allowed(rule, role)
+ allowed = authority.allowed(rule, roles)
self.assertTrue(allowed)
for rule in disallowed_rules:
- allowed = authority.allowed(rule, role)
+ allowed = authority.allowed(rule, roles)
self.assertFalse(allowed)
def test_admin_policy_file_with_member_role(self):
@@ -148,7 +209,7 @@
authority = policy_authority.PolicyAuthority(
test_tenant_id, test_user_id, "admin_rbac_policy")
- role = 'Member'
+ roles = ['Member']
allowed_rules = [
'non_admin_rule'
]
@@ -156,11 +217,11 @@
'admin_rule', 'is_admin_rule', 'alt_admin_rule']
for rule in allowed_rules:
- allowed = authority.allowed(rule, role)
+ allowed = authority.allowed(rule, roles)
self.assertTrue(allowed)
for rule in disallowed_rules:
- allowed = authority.allowed(rule, role)
+ allowed = authority.allowed(rule, roles)
self.assertFalse(allowed)
def test_alt_admin_policy_file_with_context_is_admin(self):
@@ -169,28 +230,28 @@
authority = policy_authority.PolicyAuthority(
test_tenant_id, test_user_id, "alt_admin_rbac_policy")
- role = 'fake_admin'
+ roles = ['fake_admin']
allowed_rules = ['non_admin_rule']
disallowed_rules = ['admin_rule']
for rule in allowed_rules:
- allowed = authority.allowed(rule, role)
+ allowed = authority.allowed(rule, roles)
self.assertTrue(allowed)
for rule in disallowed_rules:
- allowed = authority.allowed(rule, role)
+ allowed = authority.allowed(rule, roles)
self.assertFalse(allowed)
- role = 'super_admin'
+ roles = ['super_admin']
allowed_rules = ['admin_rule']
disallowed_rules = ['non_admin_rule']
for rule in allowed_rules:
- allowed = authority.allowed(rule, role)
+ allowed = authority.allowed(rule, roles)
self.assertTrue(allowed)
for rule in disallowed_rules:
- allowed = authority.allowed(rule, role)
+ allowed = authority.allowed(rule, roles)
self.assertFalse(allowed)
def test_tenant_user_policy(self):
@@ -208,17 +269,17 @@
# Check whether Member role can perform expected actions.
allowed_rules = ['rule1', 'rule2', 'rule3', 'rule4']
for rule in allowed_rules:
- allowed = authority.allowed(rule, 'Member')
+ allowed = authority.allowed(rule, ['Member'])
self.assertTrue(allowed)
disallowed_rules = ['admin_tenant_rule', 'admin_user_rule']
for disallowed_rule in disallowed_rules:
- self.assertFalse(authority.allowed(disallowed_rule, 'Member'))
+ self.assertFalse(authority.allowed(disallowed_rule, ['Member']))
# Check whether admin role can perform expected actions.
allowed_rules.extend(disallowed_rules)
for rule in allowed_rules:
- allowed = authority.allowed(rule, 'admin')
+ allowed = authority.allowed(rule, ['admin'])
self.assertTrue(allowed)
# Check whether _try_rule is called with the correct target dictionary.
@@ -243,7 +304,7 @@
}
for rule in allowed_rules:
- allowed = authority.allowed(rule, 'Member')
+ allowed = authority.allowed(rule, ['Member'])
self.assertTrue(allowed)
mock_try_rule.assert_called_once_with(
rule, expected_target, expected_access_data, mock.ANY)
@@ -292,7 +353,7 @@
fake_rule, [self.custom_policy_file], "custom_rbac_policy")
e = self.assertRaises(rbac_exceptions.RbacParsingException,
- authority.allowed, fake_rule, None)
+ authority.allowed, fake_rule, [None])
self.assertIn(expected_message, str(e))
m_log.debug.assert_called_once_with(expected_message)
@@ -309,13 +370,13 @@
mock.sentinel.error)})
expected_message = (
- 'Policy action "{0}" not found in policy files: {1} or among '
+ '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)
+ authority.allowed, [mock.sentinel.rule], [None])
self.assertIn(expected_message, str(e))
m_log.debug.assert_called_once_with(expected_message)
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 1a2c691..9e547b8 100644
--- a/patrole_tempest_plugin/tests/unit/test_rbac_rule_validation.py
+++ b/patrole_tempest_plugin/tests/unit/test_rbac_rule_validation.py
@@ -46,8 +46,33 @@
project_id=mock.sentinel.project_id)
setattr(self.mock_test_args.os_primary, 'credentials', mock_creds)
+ self.test_roles = ['member']
self.useFixture(
- patrole_fixtures.ConfPatcher(rbac_test_role='Member',
+ patrole_fixtures.ConfPatcher(rbac_test_roles=self.test_roles,
+ group='patrole'))
+ # Disable patrole log for unit tests.
+ self.useFixture(
+ patrole_fixtures.ConfPatcher(enable_reporting=False,
+ group='patrole_log'))
+
+
+class BaseRBACMultiRoleRuleValidationTest(base.TestCase):
+
+ def setUp(self):
+ super(BaseRBACMultiRoleRuleValidationTest, self).setUp()
+ self.mock_test_args = mock.Mock(spec=test.BaseTestCase)
+ self.mock_test_args.os_primary = mock.Mock(spec=manager.Manager)
+ self.mock_test_args.rbac_utils = mock.Mock(
+ spec_set=rbac_utils.RbacUtils)
+
+ # Setup credentials for mock client manager.
+ mock_creds = mock.Mock(user_id=mock.sentinel.user_id,
+ project_id=mock.sentinel.project_id)
+ setattr(self.mock_test_args.os_primary, 'credentials', mock_creds)
+
+ self.test_roles = ['member', 'anotherrole']
+ self.useFixture(
+ patrole_fixtures.ConfPatcher(rbac_test_roles=self.test_roles,
group='patrole'))
# Disable patrole log for unit tests.
self.useFixture(
@@ -118,8 +143,8 @@
def test_policy(*args):
raise exceptions.Forbidden()
- test_re = ("Role Member was not allowed to perform the following "
- "actions: \[%s\].*" % (mock.sentinel.action))
+ test_re = ("User with roles \['member'\] was not allowed to perform "
+ "the following actions: \[%s\].*" % (mock.sentinel.action))
self.assertRaisesRegex(
rbac_exceptions.RbacUnderPermissionException, test_re, test_policy,
self.mock_test_args)
@@ -127,44 +152,66 @@
@mock.patch.object(rbac_rv, 'LOG', autospec=True)
@mock.patch.object(rbac_rv, 'policy_authority', autospec=True)
- def test_rule_validation_rbac_malformed_response_positive(
+ def test_rule_validation_rbac_failed_response_body_positive(
self, mock_authority, mock_log):
- """Test RbacMalformedResponse error is thrown without permission
- passes.
+ """Test BasePatroleResponseBodyException 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.
+ Positive test case: if subclass of BasePatroleResponseBodyException is
+ thrown and the user is not allowed to perform the action, then this is
+ a success.
"""
mock_authority.PolicyAuthority.return_value.allowed.return_value =\
False
- @rbac_rv.action(mock.sentinel.service, rules=[mock.sentinel.action])
- def test_policy(*args):
- raise rbac_exceptions.RbacMalformedResponse()
+ def _do_test(exception_cls, **kwargs):
+ @rbac_rv.action(mock.sentinel.service,
+ rules=[mock.sentinel.action])
+ def test_policy(*args):
+ raise exception_cls(**kwargs)
- mock_log.error.assert_not_called()
+ mock_log.error.assert_not_called()
+ mock_log.warning.assert_not_called()
+
+ _do_test(rbac_exceptions.RbacMissingAttributeResponseBody,
+ attribute=mock.sentinel.attr)
+ _do_test(rbac_exceptions.RbacPartialResponseBody,
+ body=mock.sentinel.body)
+ _do_test(rbac_exceptions.RbacEmptyResponseBody)
@mock.patch.object(rbac_rv, 'LOG', autospec=True)
@mock.patch.object(rbac_rv, 'policy_authority', autospec=True)
- def test_rule_validation_rbac_malformed_response_negative(
+ def test_rule_validation_soft_authorization_exceptions(
self, mock_authority, mock_log):
- """Test RbacMalformedResponse error is thrown with permission fails.
+ """Test RbacUnderPermissionException error is thrown when any of the
+ soft authorization-related exceptions are raised by a test.
- Negative test case: if RbacMalformedResponse is thrown and the user is
- allowed to perform the action, then this is an expected failure.
+ Negative test case: if subclass of BasePatroleResponseBodyException is
+ thrown and the user is allowed to perform the action, then this is an
+ expected failure.
"""
mock_authority.PolicyAuthority.return_value.allowed.return_value = True
- @rbac_rv.action(mock.sentinel.service, rules=[mock.sentinel.action])
- def test_policy(*args):
- raise rbac_exceptions.RbacMalformedResponse()
+ def _do_test(exception_cls, **kwargs):
+ @rbac_rv.action(mock.sentinel.service,
+ rules=[mock.sentinel.action])
+ def test_policy(*args):
+ raise exception_cls(**kwargs)
- test_re = ("Role Member was not allowed to perform the following "
- "actions: \[%s\].*" % (mock.sentinel.action))
- self.assertRaisesRegex(
- rbac_exceptions.RbacUnderPermissionException, test_re, test_policy,
- self.mock_test_args)
- self.assertRegex(mock_log.error.mock_calls[0][1][0], test_re)
+ test_re = (".*User with roles \[%s\] was not allowed to "
+ "perform the following actions: \[%s\].*" % (
+ ', '.join("'%s'" % r for r in self.test_roles),
+ mock.sentinel.action))
+ self.assertRaisesRegex(
+ rbac_exceptions.RbacUnderPermissionException, test_re,
+ test_policy, self.mock_test_args)
+ self.assertRegex(mock_log.error.mock_calls[0][1][0], test_re)
+
+ _do_test(rbac_exceptions.RbacMissingAttributeResponseBody,
+ attribute=mock.sentinel.attr)
+ _do_test(rbac_exceptions.RbacPartialResponseBody,
+ body=mock.sentinel.body)
+ _do_test(rbac_exceptions.RbacEmptyResponseBody)
@mock.patch.object(rbac_rv, 'LOG', autospec=True)
@mock.patch.object(rbac_rv, 'policy_authority', autospec=True)
@@ -214,8 +261,8 @@
raise exceptions.NotFound()
expected_errors = [
- ("Role Member was not allowed to perform the following "
- "actions: \['%s'\].*" % policy_names[0]),
+ ("User with roles \['member'\] was not allowed to perform the "
+ "following actions: \['%s'\].*" % policy_names[0]),
None
]
@@ -348,6 +395,86 @@
mock_log.error.reset_mock()
+class RBACMultiRoleRuleValidationTest(BaseRBACMultiRoleRuleValidationTest,
+ RBACRuleValidationTest):
+ @mock.patch.object(rbac_rv, 'LOG', autospec=True)
+ @mock.patch.object(rbac_rv, 'policy_authority', autospec=True)
+ def test_rule_validation_forbidden_negative(self, mock_authority,
+ mock_log):
+ """Test RbacUnderPermissionException error is thrown and have
+ permission fails.
+
+ Negative test case: if Forbidden is thrown and the user should be
+ allowed to perform the action, then the RbacUnderPermissionException
+ exception should be raised.
+ """
+ mock_authority.PolicyAuthority.return_value.allowed.return_value = True
+
+ @rbac_rv.action(mock.sentinel.service, rules=[mock.sentinel.action])
+ def test_policy(*args):
+ raise exceptions.Forbidden()
+
+ test_re = ("User with roles \['member', 'anotherrole'\] was not "
+ "allowed to perform the following actions: \[%s\].*" %
+ (mock.sentinel.action))
+ self.assertRaisesRegex(
+ rbac_exceptions.RbacUnderPermissionException, test_re, test_policy,
+ self.mock_test_args)
+ self.assertRegex(mock_log.error.mock_calls[0][1][0], test_re)
+
+ @mock.patch.object(rbac_rv, 'LOG', autospec=True)
+ @mock.patch.object(rbac_rv, 'policy_authority', autospec=True)
+ def test_expect_not_found_and_raise_not_found(self, mock_authority,
+ mock_log):
+ """Test that expecting 404 and getting 404 works for all scenarios.
+
+ Tests the following scenarios:
+ 1) Test no permission and 404 is expected and 404 is thrown succeeds.
+ 2) Test have permission and 404 is expected and 404 is thrown fails.
+
+ In both cases, a LOG.warning is called with the "irregular message"
+ that signals to user that a 404 was expected and caught.
+ """
+ policy_names = ['foo:bar']
+
+ @rbac_rv.action(mock.sentinel.service, rules=policy_names,
+ expected_error_codes=[404])
+ def test_policy(*args):
+ raise exceptions.NotFound()
+
+ expected_errors = [
+ ("User with roles \['member', 'anotherrole'\] was not allowed to "
+ "perform the following actions: \['%s'\].*" % policy_names[0]),
+ None
+ ]
+
+ for pos, allowed in enumerate([True, False]):
+ mock_authority.PolicyAuthority.return_value.allowed\
+ .return_value = allowed
+
+ error_re = expected_errors[pos]
+
+ if error_re:
+ self.assertRaisesRegex(
+ rbac_exceptions.RbacUnderPermissionException, error_re,
+ test_policy, self.mock_test_args)
+ self.assertRegex(mock_log.error.mock_calls[0][1][0], error_re)
+ else:
+ test_policy(self.mock_test_args)
+ mock_log.error.assert_not_called()
+
+ 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",
+ test_policy.__name__,
+ ', '.join(policy_names),
+ mock.sentinel.service)
+
+ mock_log.warning.reset_mock()
+ mock_log.error.reset_mock()
+
+
class RBACRuleValidationLoggingTest(BaseRBACRuleValidationTest):
"""Test class for validating the RBAC log, dedicated to just logging
Patrole RBAC validation work flows.
@@ -422,7 +549,7 @@
policy_authority = mock_authority.PolicyAuthority.return_value
policy_authority.allowed.assert_called_with(
mock.sentinel.action,
- CONF.patrole.rbac_test_role)
+ CONF.patrole.rbac_test_roles)
mock_log.error.assert_not_called()
@@ -454,18 +581,23 @@
policy_authority = mock_authority.PolicyAuthority.return_value
policy_authority.allowed.assert_called_with(
"foo",
- CONF.patrole.rbac_test_role)
+ CONF.patrole.rbac_test_roles)
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)
+ CONF.patrole.rbac_test_roles)
mock_log.error.assert_not_called()
+class RBACMultiRoleRuleValidationLoggingTest(
+ BaseRBACMultiRoleRuleValidationTest, RBACRuleValidationLoggingTest):
+ pass
+
+
class RBACRuleValidationNegativeTest(BaseRBACRuleValidationTest):
def setUp(self):
@@ -488,6 +620,11 @@
test_policy, self.mock_test_args)
+class RBACMultiRoleRuleValidationNegativeTest(
+ BaseRBACMultiRoleRuleValidationTest, RBACRuleValidationNegativeTest):
+ pass
+
+
class RBACRuleValidationTestMultiPolicy(BaseRBACRuleValidationTest):
"""Test suite for validating multi-policy support for the
``rbac_rule_validation`` decorator.
@@ -502,7 +639,7 @@
def _assert_policy_authority_called_with(self, rules, mock_authority):
m_authority = mock_authority.PolicyAuthority.return_value
m_authority.allowed.assert_has_calls([
- mock.call(rule, CONF.patrole.rbac_test_role) for rule in rules
+ mock.call(rule, CONF.patrole.rbac_test_roles) for rule in rules
])
@mock.patch.object(rbac_rv, 'policy_authority', autospec=True)
@@ -614,10 +751,10 @@
mock_authority.PolicyAuthority.return_value.allowed\
.return_value = True
- error_re = ("Role Member was not allowed to perform the following "
- "actions: %s. Expected allowed actions: %s. Expected "
- "disallowed actions: []." % (rules, rules)).replace(
- '[', '\[').replace(']', '\]')
+ error_re = ("User with roles ['member'] was not allowed to perform "
+ "the following actions: %s. Expected allowed actions: %s. "
+ "Expected disallowed actions: []." %
+ (rules, rules)).replace('[', '\[').replace(']', '\]')
self.assertRaisesRegex(
rbac_exceptions.RbacUnderPermissionException, error_re,
test_policy, self.mock_test_args)
@@ -739,6 +876,39 @@
self.assertRaisesRegex(ValueError, error_re, _do_test, None, [404])
+class RBACMultiRoleRuleValidationTestMultiPolicy(
+ BaseRBACMultiRoleRuleValidationTest, RBACRuleValidationTestMultiPolicy):
+ @mock.patch.object(rbac_rv, 'LOG', autospec=True)
+ @mock.patch.object(rbac_rv, 'policy_authority', autospec=True)
+ def test_rule_validation_multi_policy_forbidden_failure(
+ self, mock_authority, mock_log):
+ """Test that when the expected result is authorized and the test
+ fails (with a Forbidden error code) that the overall evaluation
+ results in a RbacUnderPermissionException getting raised.
+ """
+
+ # NOTE: Avoid mock.sentinel here due to weird sorting with them.
+ rules = ['action1', 'action2', 'action3']
+
+ @rbac_rv.action(mock.sentinel.service, rules=rules,
+ expected_error_codes=[403, 403, 403])
+ def test_policy(*args):
+ raise exceptions.Forbidden()
+
+ mock_authority.PolicyAuthority.return_value.allowed\
+ .return_value = True
+
+ error_re = ("User with roles ['member', 'anotherrole'] was not "
+ "allowed to perform the following actions: %s. Expected "
+ "allowed actions: %s. Expected disallowed actions: []." %
+ (rules, rules)).replace('[', '\[').replace(']', '\]')
+ self.assertRaisesRegex(
+ rbac_exceptions.RbacUnderPermissionException, error_re,
+ test_policy, self.mock_test_args)
+ self.assertRegex(mock_log.error.mock_calls[0][1][0], error_re)
+ self._assert_policy_authority_called_with(rules, mock_authority)
+
+
class RBACOverrideRoleValidationTest(BaseRBACRuleValidationTest):
"""Class for validating that untimely exceptions (outside
``override_role`` is called) result in test failures.
@@ -793,7 +963,7 @@
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
+ valid in case of e.g. RbacPartialResponseBody) after override_role
passes.
"""
mock_authority.PolicyAuthority.return_value.allowed.return_value =\
diff --git a/patrole_tempest_plugin/tests/unit/test_rbac_utils.py b/patrole_tempest_plugin/tests/unit/test_rbac_utils.py
index 5132079..bd13e34 100644
--- a/patrole_tempest_plugin/tests/unit/test_rbac_utils.py
+++ b/patrole_tempest_plugin/tests/unit/test_rbac_utils.py
@@ -52,7 +52,7 @@
self.rbac_utils.override_role()
mock_test_obj = self.rbac_utils.mock_test_obj
- roles_client = self.rbac_utils.roles_v3_client
+ roles_client = self.rbac_utils.admin_roles_client
mock_time = self.rbac_utils.mock_time
roles_client.create_user_role_on_project.assert_called_once_with(
@@ -67,7 +67,7 @@
self.rbac_utils.set_roles(['admin', 'member'], 'admin')
self.rbac_utils.override_role()
- roles_client = self.rbac_utils.roles_v3_client
+ roles_client = self.rbac_utils.admin_roles_client
mock_time = self.rbac_utils.mock_time
roles_client.create_user_role_on_project.assert_not_called()
@@ -77,7 +77,7 @@
self.rbac_utils.override_role(True)
mock_test_obj = self.rbac_utils.mock_test_obj
- roles_client = self.rbac_utils.roles_v3_client
+ roles_client = self.rbac_utils.admin_roles_client
mock_time = self.rbac_utils.mock_time
roles_client.create_user_role_on_project.assert_has_calls([
@@ -96,7 +96,7 @@
self.rbac_utils.set_roles(['admin', 'member'], 'member')
self.rbac_utils.override_role(True)
- roles_client = self.rbac_utils.roles_v3_client
+ roles_client = self.rbac_utils.admin_roles_client
mock_time = self.rbac_utils.mock_time
roles_client.create_user_role_on_project.assert_has_calls([
@@ -109,7 +109,7 @@
self.rbac_utils.override_role(True, False)
mock_test_obj = self.rbac_utils.mock_test_obj
- roles_client = self.rbac_utils.roles_v3_client
+ roles_client = self.rbac_utils.admin_roles_client
mock_time = self.rbac_utils.mock_time
roles_client.create_user_role_on_project.assert_has_calls([
@@ -133,7 +133,7 @@
self.rbac_utils.set_roles(['admin', 'member'], ['member', 'random'])
self.rbac_utils.override_role()
- roles_client = self.rbac_utils.roles_v3_client
+ roles_client = self.rbac_utils.admin_roles_client
roles_client.list_user_roles_on_project.assert_called_once_with(
self.rbac_utils.PROJECT_ID, self.rbac_utils.USER_ID)
@@ -169,7 +169,8 @@
mock_override_role.assert_called_once_with(_rbac_utils, test_obj,
False)
- @mock.patch.object(rbac_utils.RbacUtils, '_override_role', autospec=True)
+ @mock.patch.object(rbac_utils.RbacUtils, '_override_role',
+ autospec=True)
def test_override_role_context_manager_simulate_fail(self,
mock_override_role):
"""Validate that expected override_role calls are made when switching
diff --git a/patrole_tempest_plugin/tests/unit/test_requirements_authority.py b/patrole_tempest_plugin/tests/unit/test_requirements_authority.py
index 1fb9636..4b75ef5 100644
--- a/patrole_tempest_plugin/tests/unit/test_requirements_authority.py
+++ b/patrole_tempest_plugin/tests/unit/test_requirements_authority.py
@@ -38,11 +38,11 @@
def test_auth_allowed_empty_roles(self):
self.rbac_auth.roles_dict = None
self.assertRaises(exceptions.InvalidConfiguration,
- self.rbac_auth.allowed, "", "")
+ self.rbac_auth.allowed, "", [""])
def test_auth_allowed_role_in_api(self):
self.rbac_auth.roles_dict = {'api': ['_member_']}
- self.assertTrue(self.rbac_auth.allowed("api", "_member_"))
+ self.assertTrue(self.rbac_auth.allowed("api", ["_member_"]))
def test_auth_allowed_role_not_in_api(self):
self.rbac_auth.roles_dict = {'api': ['_member_']}
@@ -64,7 +64,8 @@
self.rbac_auth.roles_dict = req_auth.RequirementsParser.parse("Test")
self.assertEqual(self.expected_result, self.rbac_auth.roles_dict)
- self.assertTrue(self.rbac_auth.allowed("test:create2", "test_member"))
+ self.assertTrue(
+ self.rbac_auth.allowed("test:create2", ["test_member"]))
def test_parser_role_not_in_api(self):
req_auth.RequirementsParser.Inner._rbac_map = \
@@ -82,4 +83,4 @@
self.assertIsNone(self.rbac_auth.roles_dict)
self.assertRaises(exceptions.InvalidConfiguration,
- self.rbac_auth.allowed, "", "")
+ self.rbac_auth.allowed, "", [""])
diff --git a/releasenotes/notes/break-up-rbac-malformed-exception-into-discrete-exceptions-92aedb99d0a13f58.yaml b/releasenotes/notes/break-up-rbac-malformed-exception-into-discrete-exceptions-92aedb99d0a13f58.yaml
new file mode 100644
index 0000000..0a93b64
--- /dev/null
+++ b/releasenotes/notes/break-up-rbac-malformed-exception-into-discrete-exceptions-92aedb99d0a13f58.yaml
@@ -0,0 +1,25 @@
+---
+features:
+ - |
+ The exception class ``RbacMalformedException`` has been broken up into the
+ following discrete exceptions:
+
+ * ``RbacMissingAttributeResponseBody`` - incomplete means that the
+ response body (for show or list) is missing certain attributes
+ * ``RbacPartialResponseBody`` - partial means that a list response
+ only returned a subset of the possible results available.
+ * ``RbacEmptyResponseBody`` - empty means that the show or list
+ response body is entirely empty
+
+ Each of the exception classes above deals with a different type of failure
+ related to a soft authorization failure. This means that, rather than a
+ 403 error code getting returned by the server, the response body is
+ incomplete in some way.
+upgrade:
+ - |
+ The exception class ``RbacMalformedException`` has been removed. Use one
+ of the following exception classes instead:
+
+ * ``RbacMissingAttributeResponseBody``
+ * ``RbacPartialResponseBody``
+ * ``RbacEmptyResponseBody``
diff --git a/releasenotes/notes/deprecate-roles-client-in-rbac-utils-087eda0658d18fa9.yaml b/releasenotes/notes/deprecate-roles-client-in-rbac-utils-087eda0658d18fa9.yaml
new file mode 100644
index 0000000..c42da8a
--- /dev/null
+++ b/releasenotes/notes/deprecate-roles-client-in-rbac-utils-087eda0658d18fa9.yaml
@@ -0,0 +1,6 @@
+---
+deprecations:
+ - |
+ Patrole will only support the v3 Tempest roles client for role
+ overriding operations. Support for the v2 version has been dropped
+ because the Keystone v2 API is slated for removal.
diff --git a/releasenotes/notes/multi-role-rbac-7f597c004a558956.yaml b/releasenotes/notes/multi-role-rbac-7f597c004a558956.yaml
new file mode 100644
index 0000000..20c6c0e
--- /dev/null
+++ b/releasenotes/notes/multi-role-rbac-7f597c004a558956.yaml
@@ -0,0 +1,11 @@
+---
+features:
+ - |
+ We have replaced CONF.patrole.rbac_test_role with
+ CONF.patrole.rbac_test_roles, where instead of single role we can specify
+ list of roles to be assigned to test user. This way we may run rbac tests
+ for scenarios that requires user to have more that a single role.
+deprecations:
+ - |
+ Config parameter CONF.rbac_test_role is deprecated in favor of
+ CONF.rbac_test_roles that implements a list of roles instead of single role.
diff --git a/releasenotes/notes/removed-keystone-policies-stein-feature-flag-6cfebbf64ed525d7.yaml b/releasenotes/notes/removed-keystone-policies-stein-feature-flag-6cfebbf64ed525d7.yaml
new file mode 100644
index 0000000..3bed287
--- /dev/null
+++ b/releasenotes/notes/removed-keystone-policies-stein-feature-flag-6cfebbf64ed525d7.yaml
@@ -0,0 +1,8 @@
+---
+features:
+ - |
+ Added new feature flag called ``removed_keystone_policies_stein`` under
+ the configuration group ``[policy-feature-enabled]`` for skipping Keystone
+ tests whose policies were removed in Stein. This feature flag is currently
+ applied to credentials-related policies, e.g.:
+ identity:[create|update|get|delete]_credential
diff --git a/setup.cfg b/setup.cfg
index 02ce831..77a039a 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -26,20 +26,6 @@
[upload_sphinx]
upload-dir = doc/build/html
-[compile_catalog]
-directory = patrole/locale
-domain = patrole
-
-[update_catalog]
-domain = patrole
-output_dir = patrole/locale
-input_file = patrole/locale/patrole.pot
-
-[extract_messages]
-keywords = _ gettext ngettext l_ lazy_gettext
-mapping_file = babel.cfg
-output_file = patrole/locale/patrole.pot
-
[build_releasenotes]
all_files = 1
build-dir = releasenotes/build
diff --git a/tox.ini b/tox.ini
index ea9abf1..bc829d2 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,5 +1,5 @@
[tox]
-minversion = 1.6
+minversion = 2.0
envlist = pep8,py35,py27
skipsdist = True