Merge "Fix test_volume_upload BadRequest error"
diff --git a/.zuul.yaml b/.zuul.yaml
index 2619ed7..60f0d05 100644
--- a/.zuul.yaml
+++ b/.zuul.yaml
@@ -1,7 +1,7 @@
- job:
name: patrole-base
parent: devstack-tempest
- description: Patrole base job for admin and Member roles.
+ description: Patrole base job for admin and member roles.
required-projects:
- name: openstack/tempest
- name: openstack/patrole
@@ -54,7 +54,7 @@
- job:
name: patrole-member
parent: patrole-base
- description: Patrole job for Member role.
+ description: Patrole job for member role.
# This currently works from stable/pike onward.
branches:
- master
@@ -62,7 +62,7 @@
- stable/pike
vars:
devstack_localrc:
- RBAC_TEST_ROLE: Member
+ RBAC_TEST_ROLE: member
- job:
name: patrole-member-queens
@@ -93,12 +93,12 @@
- job:
name: patrole-py35-member
parent: patrole-base
- description: Patrole py3 job for Member role.
+ description: Patrole py35 job for member role.
vars:
devstack_localrc:
- # Use Member for py3 because arguably negative testing is more
+ # 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_ROLE: member
USE_PYTHON3: true
devstack_services:
s-account: false
@@ -123,7 +123,3 @@
jobs:
- patrole-admin
- patrole-member
- - patrole-member-queens
- - patrole-member-pike
- - patrole-py35-member
- - openstack-tox-lower-constraints
diff --git a/devstack/plugin.sh b/devstack/plugin.sh
index d56c963..bd0068b 100644
--- a/devstack/plugin.sh
+++ b/devstack/plugin.sh
@@ -13,16 +13,13 @@
function install_patrole_tempest_plugin {
setup_package $PATROLE_DIR -e
- if [[ "$RBAC_TEST_ROLE" == "member" ]]; then
- RBAC_TEST_ROLE="Member"
- fi
-
- iniset $TEMPEST_CONFIG patrole enable_rbac True
- iniset $TEMPEST_CONFIG patrole rbac_test_role $RBAC_TEST_ROLE
-
if [[ ${DEVSTACK_SERIES} == 'pike' ]]; then
+ if [[ "$RBAC_TEST_ROLE" == "member" ]]; then
+ RBAC_TEST_ROLE="Member"
+ fi
+
# Policies used by Patrole testing that were changed in a backwards-incompatible way.
- # TODO(fmontei): Remove these once stable/pike becomes EOL.
+ # TODO(felipemonteiro): Remove these once stable/pike becomes EOL.
iniset $TEMPEST_CONFIG policy-feature-enabled create_port_fixed_ips_ip_address_policy False
iniset $TEMPEST_CONFIG policy-feature-enabled update_port_fixed_ips_ip_address_policy False
iniset $TEMPEST_CONFIG policy-feature-enabled limits_extension_used_limits_policy False
@@ -30,6 +27,15 @@
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
fi
+
+ if [[ ${DEVSTACK_SERIES} == 'queens' ]]; then
+ if [[ "$RBAC_TEST_ROLE" == "member" ]]; then
+ RBAC_TEST_ROLE="Member"
+ fi
+ fi
+
+ iniset $TEMPEST_CONFIG patrole enable_rbac True
+ iniset $TEMPEST_CONFIG patrole rbac_test_role $RBAC_TEST_ROLE
}
if is_service_enabled tempest; then
diff --git a/doc/source/framework/policy_authority.rst b/doc/source/framework/policy_authority.rst
index 7cd4421..822c7b6 100644
--- a/doc/source/framework/policy_authority.rst
+++ b/doc/source/framework/policy_authority.rst
@@ -9,7 +9,8 @@
This module is only called for calculating the "Expected" result if
``[patrole] test_custom_requirements`` is ``False``.
-Using the Policy Authority Module, policy verification is performed by:
+Using the :class:`~patrole_tempest_plugin.policy_authority.PolicyAuthority`
+class, policy verification is performed by:
#. Pooling together the default `in-code` policy rules.
#. Overriding the defaults with custom policy rules located in a policy.json,
@@ -22,9 +23,40 @@
#. Performing a call with all necessary data to ``oslo.policy`` and returning
the expected result back to ``rbac_rule_validation`` decorator.
+When to use
+-----------
+
+This :class:`~patrole_tempest_plugin.rbac_authority.RbacAuthority` class
+can be used to validate the default OpenStack policy configuration. It
+is recommended that this approach be used for RBAC validation for clouds that
+use little to no policy customizations or overrides.
+
+This validation approach should be used when:
+
+* Validating the out-of-the-box policy-in-code OpenStack policy configuration.
+
+ It is important that the default OpenStack policy configuration be validated
+ before deploying OpenStack into production. Bugs exist in software and the
+ earlier they can be caught and prevented (via CI/CD, for example), the
+ better. Patrole continues to be used to identify default policy bugs
+ across OpenStack services.
+
+* Validating policy reliably and accurately.
+
+ Relying on ``oslo.policy`` to compute the expected test results provides
+ accurate tests, without the hassle of having to reinvent the wheel. Since
+ OpenStack APIs use ``oslo.policy`` for policy enforcement, it makes sense
+ to compute expected results by using the same library, ensuring test
+ reliability.
+
+* Continuously validating policy changes to OpenStack projects under
+ development by gating them against Patrole CI/CD jobs run by `Zuul`_.
+
+.. _Zuul: https://docs.openstack.org/infra/zuul/
+
Implementation
--------------
.. automodule:: patrole_tempest_plugin.policy_authority
:members:
- :special-members:
+ :undoc-members:
diff --git a/doc/source/framework/rbac_authority.rst b/doc/source/framework/rbac_authority.rst
new file mode 100644
index 0000000..84c372b
--- /dev/null
+++ b/doc/source/framework/rbac_authority.rst
@@ -0,0 +1,37 @@
+.. rbac-authority:
+
+RBAC Authority Module
+=====================
+
+Overview
+--------
+
+This module implements an abstract class that is implemented by the classes
+below. Each implementation is used by the :ref:`rbac-validation` framework
+to determine each expected test result.
+
+:ref:`policy-authority`
+-----------------------
+
+The *default* :class:`~patrole_tempest_plugin.rbac_authority.RbacAuthority`
+implementation class which is used for policy validation. Uses ``oslo.policy``
+to determine the expected test result.
+
+All Patrole `Zuul`_ gates use this
+:class:`~patrole_tempest_plugin.rbac_authority.RbacAuthority` class by default.
+
+.. _Zuul: https://docs.openstack.org/infra/zuul/
+
+:ref:`requirements-authority`
+-----------------------------
+
+Optional :class:`~patrole_tempest_plugin.rbac_authority.RbacAuthority`
+implementation class which is used for policy validation. It uses a high-level
+requirements-driven approach to validating RBAC in Patrole.
+
+Implementation
+--------------
+
+.. automodule:: patrole_tempest_plugin.rbac_authority
+ :members:
+ :undoc-members:
diff --git a/doc/source/framework/requirements_authority.rst b/doc/source/framework/requirements_authority.rst
new file mode 100644
index 0000000..6c4fcc0
--- /dev/null
+++ b/doc/source/framework/requirements_authority.rst
@@ -0,0 +1,105 @@
+.. _requirements-authority:
+
+Requirements Authority Module
+=============================
+
+Overview
+--------
+
+Requirements-driven approach to declaring the expected RBAC test results
+referenced by Patrole. Uses a high-level YAML syntax to crystallize policy
+requirements concisely and unambiguously.
+
+.. note::
+
+ The :ref:`custom-requirements-file` is required to use this validation
+ approach and, currently, must be manually generated.
+
+This validation approach can be toggled on by setting the
+``[patrole].test_custom_requirements`` configuration option to ``True``;
+see :ref:`patrole-configuration` for more information.
+
+When to use
+-----------
+
+This :class:`~patrole_tempest_plugin.rbac_authority.RbacAuthority` class
+can be used to achieve a requirements-driven approach to validating an
+OpenStack cloud's RBAC implementation. Using this approach, Patrole computes
+expected test results by performing lookups against a
+:ref:`custom-requirements-file` which precisely defines the cloud's RBAC
+requirements.
+
+Using a high-level declarative language, the requirements are captured
+unambiguously in the :ref:`custom-requirements-file`, allowing operators to
+validate their requirements against their OpenStack cloud.
+
+This validation approach should be used when:
+
+* The cloud has heavily customized policy files that require careful validation
+ against one's requirements.
+
+ Heavily customized policy files can contain relatively nuanced/technical
+ syntax that impinges upon the goal of using a clear and concise syntax
+ present in the :ref:`custom-requirements-file` to drive RBAC validation.
+
+* The cloud has non-OpenStack services that require RBAC validation but which
+ don't leverage the ``oslo.policy`` framework.
+
+ Services like `Contrail`_ that are present in an OpenStack-based cloud that
+ interface with OpenStack services like Neutron also require RBAC validation.
+ The requirements-driven approach to RBAC validation is framework-agnostic
+ and so can work with any policy engine.
+
+* Expected results are captured as clear-cut, unambiguous requirements.
+
+ Validating a cloud's RBAC against high-level, clear-cut requirements is
+ a valid use case. Relying on ``oslo.policy`` validating customized policy
+ files is not sufficient to satisfy this use case.
+
+As mentioned above, the trade-off with this approach is having to manually
+generate the :ref:`custom-requirements-file`. There is currently no
+tooling to automatically do this.
+
+.. _Contrail: https://github.com/Juniper/contrail-controller/wiki/RBAC
+
+.. _custom-requirements-file:
+
+Custom Requirements File
+^^^^^^^^^^^^^^^^^^^^^^^^
+
+File path of the YAML file that defines your RBAC requirements. This
+file must be located on the same host that Patrole runs on. The YAML
+file should be written as follows:
+
+.. code-block:: yaml
+
+ <service_foo>:
+ <api_action_a>:
+ - <allowed_role_1>
+ - <allowed_role_2>
+ - <allowed_role_3>
+ <api_action_b>:
+ - <allowed_role_2>
+ - <allowed_role_4>
+ <service_bar>:
+ <api_action_c>:
+ - <allowed_role_3>
+
+Where:
+
+service = the service that is being tested (Cinder, Nova, etc.).
+
+api_action = the policy action that is being tested. Examples:
+
+* volume:create
+* os_compute_api:servers:start
+* add_image
+
+allowed_role = the ``oslo.policy`` role that is allowed to perform the API.
+
+Implementation
+--------------
+
+.. automodule:: patrole_tempest_plugin.requirements_authority
+ :members:
+ :undoc-members:
diff --git a/doc/source/index.rst b/doc/source/index.rst
index d964845..8368262 100644
--- a/doc/source/index.rst
+++ b/doc/source/index.rst
@@ -47,7 +47,9 @@
framework/overview
framework/rbac_validation
+ framework/rbac_authority
framework/policy_authority
+ framework/requirements_authority
framework/rbac_utils
Indices and tables
diff --git a/etc/patrole.conf.sample b/etc/patrole.conf.sample
index 5816ea9..8e7931b 100644
--- a/etc/patrole.conf.sample
+++ b/etc/patrole.conf.sample
@@ -28,15 +28,16 @@
#
# This option determines whether Patrole should run against a
-# `custom_requirements_file` which defines RBAC requirements. The
-# purpose of setting this flag to True is to verify that RBAC policy
+# ``custom_requirements_file`` which defines RBAC requirements. The
+# purpose of setting this flag to ``True`` is to verify that RBAC
+# policy
# is in accordance to requirements. The idea is that the
-# `custom_requirements_file` perfectly defines what the RBAC
+# ``custom_requirements_file`` precisely defines what the RBAC
# requirements are.
#
# Here are the possible outcomes when running the Patrole tests
# against
-# a `custom_requirements_file`:
+# a ``custom_requirements_file``:
#
# YAML definition: allowed
# test run: allowed
@@ -44,7 +45,7 @@
#
# YAML definition: allowed
# test run: not allowed
-# test result: fail (under-permission)
+# test result: fail (under-permission, e.g. Forbidden exception)
#
# YAML definition: not allowed
# test run: allowed
@@ -53,30 +54,36 @@
#test_custom_requirements = false
#
-# File path of the yaml file that defines your RBAC requirements. This
-# file must be located on the same host that Patrole runs on. The yaml
+# File path of the YAML file that defines your RBAC requirements. This
+# file must be located on the same host that Patrole runs on. The YAML
# file should be written as follows:
#
-# ```
-# <service>:
-# <api_action>:
-# - <allowed_role>
-# - <allowed_role>
-# - <allowed_role>
-# <api_action>:
-# - <allowed_role>
-# - <allowed_role>
-# <service>
-# <api_action>:
-# - <allowed_role>
-# ```
+# .. code-block:: yaml
+#
+# <service_foo>:
+# <api_action_a>:
+# - <allowed_role_1>
+# - <allowed_role_2>
+# - <allowed_role_3>
+# <api_action_b>:
+# - <allowed_role_2>
+# - <allowed_role_4>
+# <service_bar>:
+# <api_action_c>:
+# - <allowed_role_3>
+#
# Where:
-# service = the service that is being tested (cinder, nova, etc)
+#
+# service = the service that is being tested (Cinder, Nova, etc.).
+#
# api_action = the policy action that is being tested. Examples:
-# - volume:create
-# - os_compute_api:servers:start
-# - add_image
-# allowed_role = the Keystone role that is allowed to perform the API
+#
+# * volume:create
+# * os_compute_api:servers:start
+# * add_image
+#
+# allowed_role = the ``oslo.policy`` role that is allowed to perform
+# the API.
# (string value)
#custom_requirements_file = <None>
diff --git a/patrole_tempest_plugin/README.rst b/patrole_tempest_plugin/README.rst
deleted file mode 100644
index d678422..0000000
--- a/patrole_tempest_plugin/README.rst
+++ /dev/null
@@ -1,5 +0,0 @@
-==============================
-Tempest Integration of Patrole
-==============================
-
-This directory contains Tempest tests to cover the Patrole project.
diff --git a/patrole_tempest_plugin/config.py b/patrole_tempest_plugin/config.py
index 5103888..ee7a6c5 100644
--- a/patrole_tempest_plugin/config.py
+++ b/patrole_tempest_plugin/config.py
@@ -41,13 +41,13 @@
default=False,
help="""
This option determines whether Patrole should run against a
-`custom_requirements_file` which defines RBAC requirements. The
-purpose of setting this flag to True is to verify that RBAC policy
+``custom_requirements_file`` which defines RBAC requirements. The
+purpose of setting this flag to ``True`` is to verify that RBAC policy
is in accordance to requirements. The idea is that the
-`custom_requirements_file` perfectly defines what the RBAC requirements are.
+``custom_requirements_file`` precisely defines what the RBAC requirements are.
Here are the possible outcomes when running the Patrole tests against
-a `custom_requirements_file`:
+a ``custom_requirements_file``:
YAML definition: allowed
test run: allowed
@@ -55,7 +55,7 @@
YAML definition: allowed
test run: not allowed
-test result: fail (under-permission)
+test result: fail (under-permission, e.g. Forbidden exception)
YAML definition: not allowed
test run: allowed
@@ -63,30 +63,35 @@
"""),
cfg.StrOpt('custom_requirements_file',
help="""
-File path of the yaml file that defines your RBAC requirements. This
-file must be located on the same host that Patrole runs on. The yaml
+File path of the YAML file that defines your RBAC requirements. This
+file must be located on the same host that Patrole runs on. The YAML
file should be written as follows:
-```
-<service>:
- <api_action>:
- - <allowed_role>
- - <allowed_role>
- - <allowed_role>
- <api_action>:
- - <allowed_role>
- - <allowed_role>
-<service>
- <api_action>:
- - <allowed_role>
-```
+.. code-block:: yaml
+
+ <service_foo>:
+ <api_action_a>:
+ - <allowed_role_1>
+ - <allowed_role_2>
+ - <allowed_role_3>
+ <api_action_b>:
+ - <allowed_role_2>
+ - <allowed_role_4>
+ <service_bar>:
+ <api_action_c>:
+ - <allowed_role_3>
+
Where:
-service = the service that is being tested (cinder, nova, etc)
+
+service = the service that is being tested (Cinder, Nova, etc.).
+
api_action = the policy action that is being tested. Examples:
- - volume:create
- - os_compute_api:servers:start
- - add_image
-allowed_role = the Keystone role that is allowed to perform the API
+
+* volume:create
+* os_compute_api:servers:start
+* add_image
+
+allowed_role = the ``oslo.policy`` role that is allowed to perform the API.
""")
]
diff --git a/patrole_tempest_plugin/policy_authority.py b/patrole_tempest_plugin/policy_authority.py
index 99348b9..b813f88 100644
--- a/patrole_tempest_plugin/policy_authority.py
+++ b/patrole_tempest_plugin/policy_authority.py
@@ -24,8 +24,8 @@
from tempest.common import credentials_factory as credentials
from tempest import config
+from patrole_tempest_plugin.rbac_authority import RbacAuthority
from patrole_tempest_plugin import rbac_exceptions
-from patrole_tempest_plugin.rbac_utils import RbacAuthority
CONF = config.CONF
LOG = logging.getLogger(__name__)
@@ -156,9 +156,9 @@
def allowed(self, rule_name, role):
"""Checks if a given rule in a policy is allowed with given role.
- :param string rule_name: Rule to be checked using ``oslo.policy``.
- :param bool is_admin: Whether admin context is used.
- :raises RbacParsingException: If `rule_name`` does not exist in the
+ :param string rule_name: Policy name to pass to``oslo.policy``.
+ :param string role: Role 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)
@@ -274,9 +274,9 @@
access_data['is_admin'] = is_admin
# TODO(felipemonteiro): Dynamically calculate is_admin_project rather
# than hard-coding it to True. is_admin_project cannot be determined
- # from the role, but rather from project and domain names. See
- # _populate_is_admin_project in keystone.token.providers.common
- # for more information.
+ # from the role, but rather from project and domain names. For more
+ # information, see:
+ # https://github.com/openstack/keystone/blob/37ce5417418f8acbd27f3dacb70c605b0fe48301/keystone/token/providers/common.py#L150
access_data['is_admin_project'] = True
class Object(object):
diff --git a/patrole_tempest_plugin/rbac_authority.py b/patrole_tempest_plugin/rbac_authority.py
new file mode 100644
index 0000000..294ecc5
--- /dev/null
+++ b/patrole_tempest_plugin/rbac_authority.py
@@ -0,0 +1,38 @@
+# Copyright 2017 AT&T Corporation.
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import abc
+
+import six
+
+
+@six.add_metaclass(abc.ABCMeta)
+class RbacAuthority(object):
+ """Class for validating whether a given role can perform a policy action.
+
+ Any class that extends ``RbacAuthority`` provides the logic for determining
+ whether a role has permissions to execute a policy action.
+ """
+
+ @abc.abstractmethod
+ def allowed(self, rule, role):
+ """Determine whether the role should be able to perform the API.
+
+ :param rule: The name of the policy enforced by the API.
+ :param role: The role used to determine whether ``rule`` can be
+ executed.
+ :returns: True if the ``role`` has permissions to execute
+ ``rule``, else False.
+ """
diff --git a/patrole_tempest_plugin/rbac_exceptions.py b/patrole_tempest_plugin/rbac_exceptions.py
index 5ee65ae..e75b8ec 100644
--- a/patrole_tempest_plugin/rbac_exceptions.py
+++ b/patrole_tempest_plugin/rbac_exceptions.py
@@ -38,7 +38,7 @@
class RbacResourceSetupFailed(exceptions.TempestException):
- message = "Rbac resource setup failed"
+ message = "RBAC resource setup failed"
class RbacOverPermission(exceptions.TempestException):
diff --git a/patrole_tempest_plugin/rbac_rule_validation.py b/patrole_tempest_plugin/rbac_rule_validation.py
index d3213cf..7d48870 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
@@ -32,11 +33,14 @@
LOG = logging.getLogger(__name__)
_SUPPORTED_ERROR_CODES = [403, 404]
+_DEFAULT_ERROR_CODE = 403
RBACLOG = logging.getLogger('rbac_reporting')
-def action(service, rule='', expected_error_code=403, extra_target_data=None):
+def action(service, rule='', rules=None,
+ expected_error_code=_DEFAULT_ERROR_CODE, expected_error_codes=None,
+ extra_target_data=None):
"""A decorator for verifying OpenStack policy enforcement.
A decorator which allows for positive and negative RBAC testing. Given:
@@ -67,15 +71,18 @@
As such, negative and positive testing can be applied using this decorator.
- :param service: An OpenStack service. Examples: "nova" or "neutron".
- :param rule: A policy action defined in a policy.json file (or in
- code).
+ :param str service: An OpenStack service. Examples: "nova" or "neutron".
+ :param str rule: (DEPRECATED) A policy action defined in a policy.json file
+ or in code.
+ :param list rules: A list of policy actions defined in a policy.json file
+ or in code. The rules are logical-ANDed together to derive the expected
+ result.
.. note::
Patrole currently only supports custom JSON policy files.
- :param expected_error_code: Overrides default value of 403 (Forbidden)
+ :param int expected_error_code: Overrides default value of 403 (Forbidden)
with endpoint-specific error code. Currently only supports 403 and 404.
Support for 404 is needed because some services, like Neutron,
intentionally throw a 404 for security reasons.
@@ -85,11 +92,30 @@
A 404 should not be provided *unless* the endpoint masks a
``Forbidden`` exception as a ``NotFound`` exception.
- :param extra_target_data: Dictionary, keyed with ``oslo.policy`` generic
- check names, whose values are string literals that reference nested
- ``tempest.test.BaseTestCase`` attributes. Used by ``oslo.policy`` for
- performing matching against attributes that are sent along with the API
- calls. Example::
+ :param list expected_error_codes: When the ``rules`` list parameter is
+ used, then this list indicates the expected error code to use if one
+ of the rules does not allow the role being tested. This list must
+ coincide with and its elements remain in the same order as the rules
+ in the rules list.
+
+ Example::
+ rules=["api_action1", "api_action2"]
+ expected_error_codes=[404, 403]
+
+ a) If api_action1 fails and api_action2 passes, then the expected
+ error code is 404.
+ b) if api_action2 fails and api_action1 passes, then the expected
+ error code is 403.
+ c) if both api_action1 and api_action2 fail, then the expected error
+ code is the first error seen (404).
+
+ If an error code is missing from the list, it is defaulted to 403.
+
+ :param dict extra_target_data: Dictionary, keyed with ``oslo.policy``
+ generic check names, whose values are string literals that reference
+ nested ``tempest.test.BaseTestCase`` attributes. Used by
+ ``oslo.policy`` for performing matching against attributes that are
+ sent along with the API calls. Example::
extra_target_data={
"target.token.user_id":
@@ -113,6 +139,10 @@
if extra_target_data is None:
extra_target_data = {}
+ rules, expected_error_codes = _prepare_multi_policy(rule, rules,
+ expected_error_code,
+ expected_error_codes)
+
def decorator(test_func):
role = CONF.patrole.rbac_test_role
@@ -125,11 +155,27 @@
'`rbac_rule_validation` decorator can only be applied to '
'an instance of `tempest.test.BaseTestCase`.')
- allowed = _is_authorized(test_obj, service, rule,
- extra_target_data)
+ allowed = True
+ disallowed_rules = []
+ for rule in rules:
+ _allowed = _is_authorized(
+ test_obj, service, rule, extra_target_data)
+ if not _allowed:
+ disallowed_rules.append(rule)
+ allowed = allowed and _allowed
+
+ exp_error_code = expected_error_code
+ if disallowed_rules:
+ # Choose the first disallowed rule and expect the error
+ # code corresponding to it.
+ first_error_index = rules.index(disallowed_rules[0])
+ exp_error_code = expected_error_codes[first_error_index]
+ LOG.debug("%s: Expecting %d to be raised for policy name: %s",
+ test_func.__name__, exp_error_code,
+ disallowed_rules[0])
expected_exception, irregular_msg = _get_exception_type(
- expected_error_code)
+ exp_error_code)
test_status = 'Allowed'
@@ -148,8 +194,12 @@
if irregular_msg:
LOG.warning(irregular_msg.format(rule, service))
if allowed:
- msg = ("Role %s was not allowed to perform %s." %
- (role, rule))
+ msg = ("Role %s was not allowed to perform the following "
+ "actions: %s. Expected allowed actions: %s. "
+ "Expected disallowed actions: %s." % (
+ role, sorted(rules),
+ sorted(set(rules) - set(disallowed_rules)),
+ sorted(disallowed_rules)))
LOG.error(msg)
raise exceptions.Forbidden(
"%s Exception was: %s" % (msg, e))
@@ -164,10 +214,14 @@
LOG.error(msg)
else:
if not allowed:
- LOG.error("Role %s was allowed to perform %s", role, rule)
- raise rbac_exceptions.RbacOverPermission(
- "OverPermission: Role %s was allowed to perform %s" %
- (role, rule))
+ msg = (
+ "OverPermission: Role %s was allowed to perform the "
+ "following disallowed actions: %s" % (
+ role, sorted(disallowed_rules)
+ )
+ )
+ LOG.error(msg)
+ raise rbac_exceptions.RbacOverPermission(msg)
finally:
if CONF.patrole_log.enable_reporting:
RBACLOG.info(
@@ -181,6 +235,59 @@
return decorator
+def _prepare_multi_policy(rule, rules, exp_error_code, exp_error_codes):
+
+ if exp_error_codes:
+ if not rules:
+ msg = ("The `rules` list must be provided if using the "
+ "`expected_error_codes` list.")
+ raise ValueError(msg)
+ if len(rules) != len(exp_error_codes):
+ msg = ("The `expected_error_codes` list is not the same length "
+ "as the `rules` list.")
+ raise ValueError(msg)
+ if exp_error_code:
+ deprecation_msg = (
+ "The `exp_error_code` argument has been deprecated in favor "
+ "of `exp_error_codes` and will be removed in a future "
+ "version.")
+ versionutils.report_deprecated_feature(LOG, deprecation_msg)
+ LOG.debug("The `exp_error_codes` argument will be used instead of "
+ "`exp_error_code`.")
+ if not isinstance(exp_error_codes, (tuple, list)):
+ exp_error_codes = [exp_error_codes]
+ else:
+ exp_error_codes = []
+ if exp_error_code:
+ exp_error_codes.append(exp_error_code)
+
+ if rules is None:
+ rules = []
+ elif not isinstance(rules, (tuple, list)):
+ rules = [rules]
+ if rule:
+ deprecation_msg = (
+ "The `rule` argument has been deprecated in favor of `rules` "
+ "and will be removed in a future version.")
+ versionutils.report_deprecated_feature(LOG, deprecation_msg)
+ if rules:
+ LOG.debug("The `rules` argument will be used instead of `rule`.")
+ else:
+ rules.append(rule)
+
+ # Fill in the exp_error_codes if needed. This is needed for the scenarios
+ # where no exp_error_codes array is provided, so the error codes must be
+ # set to the default error code value and there must be the same number
+ # of error codes as rules.
+ num_ecs = len(exp_error_codes)
+ num_rules = len(rules)
+ if (num_ecs < num_rules):
+ for i in range(num_rules - num_ecs):
+ exp_error_codes.append(_DEFAULT_ERROR_CODE)
+
+ return rules, exp_error_codes
+
+
def _is_authorized(test_obj, service, rule, extra_target_data):
"""Validates whether current RBAC role has permission to do policy action.
diff --git a/patrole_tempest_plugin/rbac_utils.py b/patrole_tempest_plugin/rbac_utils.py
index 347f77f..6c40aa1 100644
--- a/patrole_tempest_plugin/rbac_utils.py
+++ b/patrole_tempest_plugin/rbac_utils.py
@@ -13,9 +13,7 @@
# License for the specific language governing permissions and limitations
# under the License.
-import abc
from contextlib import contextmanager
-import six
import time
from oslo_log import log as logging
@@ -149,18 +147,25 @@
test_obj.os_primary.auth_provider.set_auth()
def _get_roles_by_name(self):
- available_roles = self.admin_roles_client.list_roles()
- admin_role_id = rbac_role_id = None
+ available_roles = self.admin_roles_client.list_roles()['roles']
+ role_map = {r['name']: r['id'] for r in available_roles}
+ LOG.debug('Available roles: %s', list(role_map.keys()))
- for role in available_roles['roles']:
- if role['name'] == CONF.patrole.rbac_test_role:
- rbac_role_id = role['id']
- if role['name'] == CONF.identity.admin_role:
- admin_role_id = role['id']
+ admin_role_id = role_map.get(CONF.identity.admin_role)
+ rbac_role_id = role_map.get(CONF.patrole.rbac_test_role)
if not all([admin_role_id, rbac_role_id]):
- msg = ("Roles defined by `[patrole] rbac_test_role` and "
- "`[identity] admin_role` must be defined in the system.")
+ missing_roles = []
+ msg = ("Could not find `[patrole] rbac_test_role` 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)
+ 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
@@ -228,24 +233,6 @@
:returns: True if ``rbac_test_role`` is the admin role.
"""
+ # TODO(felipemonteiro): Make this more robust via a context is admin
+ # lookup.
return CONF.patrole.rbac_test_role == CONF.identity.admin_role
-
-
-@six.add_metaclass(abc.ABCMeta)
-class RbacAuthority(object):
- """Class for validating whether a given role can perform a policy action.
-
- Any class that extends ``RbacAuthority`` provides the logic for determining
- whether a role has permissions to execute a policy action.
- """
-
- @abc.abstractmethod
- def allowed(self, rule, role):
- """Determine whether the role should be able to perform the API.
-
- :param rule: The name of the policy enforced by the API.
- :param role: The role used to determine whether ``rule`` can be
- executed.
- :returns: True if the ``role`` has permissions to execute
- ``rule``, else False.
- """
diff --git a/patrole_tempest_plugin/requirements_authority.py b/patrole_tempest_plugin/requirements_authority.py
index 2db12db..75df9f4 100644
--- a/patrole_tempest_plugin/requirements_authority.py
+++ b/patrole_tempest_plugin/requirements_authority.py
@@ -16,14 +16,17 @@
from oslo_log import log as logging
+from tempest import config
from tempest.lib import exceptions
-from patrole_tempest_plugin.rbac_utils import RbacAuthority
+from patrole_tempest_plugin.rbac_authority import RbacAuthority
+CONF = config.CONF
LOG = logging.getLogger(__name__)
class RequirementsParser(object):
+ """A class that parses a custom requirements file."""
_inner = None
class Inner(object):
@@ -40,6 +43,27 @@
@staticmethod
def parse(component):
+ """Parses a requirements file with the following format:
+
+ .. code-block:: yaml
+
+ <service_foo>:
+ <api_action_a>:
+ - <allowed_role_1>
+ - <allowed_role_2>
+ - <allowed_role_3>
+ <api_action_b>:
+ - <allowed_role_2>
+ - <allowed_role_4>
+ <service_bar>:
+ <api_action_c>:
+ - <allowed_role_3>
+
+ :param str component: Name of the OpenStack service to be validated.
+ :returns: The dictionary that maps each policy action to the list
+ of allowed roles, for the given ``component``.
+ :rtype: dict
+ """
try:
for section in RequirementsParser.Inner._rbac_map:
if component in section:
@@ -51,13 +75,39 @@
class RequirementsAuthority(RbacAuthority):
+ """A class that uses a custom requirements file to validate RBAC."""
+
def __init__(self, filepath=None, component=None):
- if filepath is not None and component is not None:
+ """This class can be used to achieve a requirements-driven approach to
+ validating an OpenStack cloud's RBAC implementation. Using this
+ approach, Patrole computes expected test results by performing lookups
+ against a custom requirements file which precisely defines the cloud's
+ RBAC requirements.
+
+ :param str filepath: Path where the custom requirements file lives.
+ Defaults to ``[patrole].custom_requirements_file``.
+ :param str component: Name of the OpenStack service to be validated.
+ """
+ filepath = filepath or CONF.patrole.custom_requirements_file
+
+ if component is not None:
self.roles_dict = RequirementsParser(filepath).parse(component)
else:
self.roles_dict = None
def allowed(self, rule_name, role):
+ """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.
+ :returns: True if ``role`` is allowed to perform ``rule_name``, else
+ False.
+ :rtype: bool
+ :raises KeyError: If ``rule_name`` does not exist among the keyed
+ policy names in the custom requirements file.
+ """
if self.roles_dict is None:
raise exceptions.InvalidConfiguration(
"Roles dictionary parsed from requirements YAML file is "
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 adb5a6c..1fe52e9 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
@@ -33,6 +33,14 @@
class ServerActionsRbacTest(rbac_base.BaseV2ComputeRbacTest):
+ # admin credentials used for waiters which invokes a show API call
+ credentials = ['primary', 'admin']
+
+ @classmethod
+ def setup_clients(cls):
+ super(ServerActionsRbacTest, cls).setup_clients()
+ cls.admin_servers_client = cls.os_admin.servers_client
+
@classmethod
def resource_setup(cls):
super(ServerActionsRbacTest, cls).resource_setup()
@@ -58,17 +66,24 @@
def _stop_server(self):
self.servers_client.stop_server(self.server_id)
waiters.wait_for_server_status(
- self.servers_client, self.server_id, 'SHUTOFF')
+ self.admin_servers_client, self.server_id, 'SHUTOFF')
def _resize_server(self, flavor):
+ status = self.admin_servers_client. \
+ show_server(self.server_id)['server']['status']
+ if status == 'RESIZED':
+ return
self.servers_client.resize_server(self.server_id, flavor)
waiters.wait_for_server_status(
- self.servers_client, self.server_id, 'VERIFY_RESIZE')
+ self.admin_servers_client, self.server_id, 'VERIFY_RESIZE')
def _confirm_resize_server(self):
- self.servers_client.confirm_resize_server(self.server_id)
+ status = self.admin_servers_client. \
+ show_server(self.server_id)['server']['status']
+ if status == 'VERIFY_RESIZE':
+ self.servers_client.confirm_resize_server(self.server_id)
waiters.wait_for_server_status(
- self.servers_client, self.server_id, 'ACTIVE')
+ self.admin_servers_client, self.server_id, 'ACTIVE')
def _shelve_server(self):
self.servers_client.shelve_server(self.server_id)
@@ -77,12 +92,13 @@
self.server_id)
offload_time = CONF.compute.shelved_offload_time
if offload_time >= 0:
- waiters.wait_for_server_status(self.servers_client,
+ waiters.wait_for_server_status(self.admin_servers_client,
self.server_id,
'SHELVED_OFFLOADED',
extra_timeout=offload_time)
else:
- waiters.wait_for_server_status(self.servers_client, self.server_id,
+ waiters.wait_for_server_status(self.admin_servers_client,
+ self.server_id,
'SHELVED')
def _pause_server(self):
@@ -91,7 +107,7 @@
self.servers_client.unpause_server,
self.server_id)
waiters.wait_for_server_status(
- self.servers_client, self.server_id, 'PAUSED')
+ self.admin_servers_client, self.server_id, 'PAUSED')
def _cleanup_server_actions(self, function, server_id, **kwargs):
server = self.servers_client.show_server(server_id)['server']
@@ -179,6 +195,7 @@
self._resize_server(self.flavor_ref_alt)
self.addCleanup(self._confirm_resize_server)
self.addCleanup(self._resize_server, self.flavor_ref)
+ self.addCleanup(self._confirm_resize_server)
with self.rbac_utils.override_role(self):
self._confirm_resize_server()
@@ -276,6 +293,7 @@
self.compute_images_client.delete_image, image['id'])
@decorators.idempotent_id('9fdd4630-731c-4f7c-bce5-69fa3792c52a')
+ @decorators.attr(type='slow')
@testtools.skipUnless(CONF.compute_feature_enabled.snapshot,
'Snapshotting not available, backup not possible.')
@utils.services('image')
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 63dee85..13faca1 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
@@ -395,7 +395,8 @@
@rbac_rule_validation.action(
service="nova",
- rule="os_compute_api:os-lock-server:unlock:unlock_override")
+ rules=["os_compute_api:os-lock-server:unlock",
+ "os_compute_api:os-lock-server:unlock:unlock_override"])
@decorators.idempotent_id('40dfeef9-73ee-48a9-be19-a219875de457')
def test_unlock_server_override(self):
"""Test force unlock server, part of os-lock-server.
@@ -569,8 +570,7 @@
@classmethod
def resource_setup(cls):
def _cleanup_ports(network_id):
- ports = cls.ports_client.\
- list_ports(network_id=network_id)['ports']
+ ports = cls.ports_client.list_ports(network_id=network_id)['ports']
for port in ports:
test_utils.call_and_ignore_notfound_exc(
cls.ports_client.delete_port,
@@ -665,6 +665,9 @@
self.interfaces_client, self.server['id'],
interface['port_id'], 'ACTIVE')
self.addCleanup(
+ waiters.wait_for_interface_detach, self.interfaces_client,
+ self.server['id'], interface['port_id'])
+ self.addCleanup(
test_utils.call_and_ignore_notfound_exc,
self.interfaces_client.delete_interface,
self.server['id'], interface['port_id'])
@@ -683,6 +686,8 @@
with self.rbac_utils.override_role(self):
self.interfaces_client.delete_interface(self.server['id'],
interface['port_id'])
+ waiters.wait_for_interface_detach(
+ self.interfaces_client, self.server['id'], interface['port_id'])
@decorators.idempotent_id('6886d360-0d86-4760-b1a3-882d81fbebcc')
@utils.requires_ext(extension='os-ips', service='compute')
@@ -722,47 +727,16 @@
if interfaces:
network_id = interfaces[0]['net_id']
else:
- network_id = self.interfaces_client.create_interface(
- self.server['id'])['interfaceAttachment']['net_id']
+ interface = self.interfaces_client.create_interface(
+ self.server['id'])['interfaceAttachment']
+ network_id = interface['net_id']
+ self.addCleanup(
+ waiters.wait_for_interface_detach, self.interfaces_client,
+ self.server['id'], interface['port_id'])
+ self.addCleanup(
+ self.interfaces_client.delete_interface,
+ self.server['id'], interface['port_id'])
with self.rbac_utils.override_role(self):
self.servers_client.add_fixed_ip(self.server['id'],
networkId=network_id)
-
-
-class VirtualInterfacesRbacTest(rbac_base.BaseV2ComputeRbacTest):
- # The compute os-virtual-interfaces API is deprecated from the Microversion
- # 2.44 onward. For more information, see:
- # https://developer.openstack.org/api-ref/compute/#servers-virtual-interfaces-servers-os-virtual-interfaces-deprecated
- max_microversion = '2.43'
-
- @classmethod
- def setup_credentials(cls):
- # This test needs a network and a subnet
- cls.set_network_resources(network=True, subnet=True)
- super(VirtualInterfacesRbacTest, cls).setup_credentials()
-
- @classmethod
- def resource_setup(cls):
- super(VirtualInterfacesRbacTest, cls).resource_setup()
- cls.server = cls.create_test_server(wait_until='ACTIVE')
-
- @rbac_rule_validation.action(
- service="nova",
- rule="os_compute_api:os-virtual-interfaces")
- @decorators.idempotent_id('fc719ae3-0f73-4689-8378-1b841f0f2818')
- def test_list_virtual_interfaces(self):
- """Test list virtual interfaces, part of os-virtual-interfaces.
-
- If Neutron is available, then call the API and expect it to fail
- with a 400 BadRequest (policy enforcement is done before that happens).
- """
- with self.rbac_utils.override_role(self):
- if CONF.service_available.neutron:
- msg = ("Listing virtual interfaces is not supported by this "
- "cloud.")
- with self.assertRaisesRegex(lib_exc.BadRequest, msg):
- self.servers_client.list_virtual_interfaces(
- self.server['id'])
- else:
- self.servers_client.list_virtual_interfaces(self.server['id'])
diff --git a/patrole_tempest_plugin/tests/api/compute/test_server_volume_attachments_rbac.py b/patrole_tempest_plugin/tests/api/compute/test_server_volume_attachments_rbac.py
index b803fe3..a510d1e 100644
--- a/patrole_tempest_plugin/tests/api/compute/test_server_volume_attachments_rbac.py
+++ b/patrole_tempest_plugin/tests/api/compute/test_server_volume_attachments_rbac.py
@@ -25,6 +25,10 @@
CONF = config.CONF
+# FIXME(felipemonteiro): `@decorators.attr(type='slow')` are added to tests
+# below to in effect cause the tests to be non-voting in Zuul due to a high
+# rate of spurious failures related to volume attachments. This will be
+# revisited at a later date.
class ServerVolumeAttachmentRbacTest(rbac_base.BaseV2ComputeRbacTest):
@classmethod
@@ -53,6 +57,7 @@
with self.rbac_utils.override_role(self):
self.servers_client.list_volume_attachments(self.server['id'])
+ @decorators.attr(type='slow')
@rbac_rule_validation.action(
service="nova",
rule="os_compute_api:os-volumes-attachments:create")
@@ -61,6 +66,7 @@
with self.rbac_utils.override_role(self):
self.attach_volume(self.server, self.volume)
+ @decorators.attr(type='slow')
@rbac_rule_validation.action(
service="nova",
rule="os_compute_api:os-volumes-attachments:show")
diff --git a/patrole_tempest_plugin/tests/api/compute/test_virtual_interfaces_rbac.py b/patrole_tempest_plugin/tests/api/compute/test_virtual_interfaces_rbac.py
new file mode 100644
index 0000000..ae77a34
--- /dev/null
+++ b/patrole_tempest_plugin/tests/api/compute/test_virtual_interfaces_rbac.py
@@ -0,0 +1,64 @@
+# Copyright 2017 AT&T Corporation.
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from tempest import config
+from tempest.lib import decorators
+from tempest.lib import exceptions as lib_exc
+
+from patrole_tempest_plugin import rbac_rule_validation
+from patrole_tempest_plugin.tests.api.compute import rbac_base
+
+CONF = config.CONF
+
+
+# TODO(rb560u): Remove this test class once the nova queens branch goes into
+# extended maintenance mode.
+class VirtualInterfacesRbacTest(rbac_base.BaseV2ComputeRbacTest):
+ # The compute os-virtual-interfaces API is deprecated from the Microversion
+ # 2.44 onward. For more information, see:
+ # https://developer.openstack.org/api-ref/compute/#servers-virtual-interfaces-servers-os-virtual-interfaces-deprecated
+ depends_on_nova_network = True
+ max_microversion = '2.43'
+
+ @classmethod
+ def setup_credentials(cls):
+ # This test needs a network and a subnet
+ cls.set_network_resources(network=True, subnet=True)
+ super(VirtualInterfacesRbacTest, cls).setup_credentials()
+
+ @classmethod
+ def resource_setup(cls):
+ super(VirtualInterfacesRbacTest, cls).resource_setup()
+ cls.server = cls.create_test_server(wait_until='ACTIVE')
+
+ @rbac_rule_validation.action(
+ service="nova",
+ rule="os_compute_api:os-virtual-interfaces")
+ @decorators.idempotent_id('fc719ae3-0f73-4689-8378-1b841f0f2818')
+ def test_list_virtual_interfaces(self):
+ """Test list virtual interfaces, part of os-virtual-interfaces.
+
+ If Neutron is available, then call the API and expect it to fail
+ with a 400 BadRequest (policy enforcement is done before that happens).
+ """
+ with self.rbac_utils.override_role(self):
+ if CONF.service_available.neutron:
+ msg = ("Listing virtual interfaces is not supported by this "
+ "cloud.")
+ with self.assertRaisesRegex(lib_exc.BadRequest, msg):
+ self.servers_client.list_virtual_interfaces(
+ self.server['id'])
+ else:
+ self.servers_client.list_virtual_interfaces(self.server['id'])
diff --git a/patrole_tempest_plugin/tests/api/identity/rbac_base.py b/patrole_tempest_plugin/tests/api/identity/rbac_base.py
index 90fa6aa..91b3d1e 100644
--- a/patrole_tempest_plugin/tests/api/identity/rbac_base.py
+++ b/patrole_tempest_plugin/tests/api/identity/rbac_base.py
@@ -118,6 +118,8 @@
@classmethod
def setup_clients(cls):
super(BaseIdentityV3RbacTest, cls).setup_clients()
+ cls.application_credentials_client = \
+ cls.os_primary.application_credentials_client
cls.creds_client = cls.os_primary.credentials_client
cls.consumers_client = cls.os_primary.oauth_consumers_client
cls.domains_client = cls.os_primary.domains_client
diff --git a/patrole_tempest_plugin/tests/api/identity/v3/test_application_credentials_rbac.py b/patrole_tempest_plugin/tests/api/identity/v3/test_application_credentials_rbac.py
new file mode 100644
index 0000000..c7a6033
--- /dev/null
+++ b/patrole_tempest_plugin/tests/api/identity/v3/test_application_credentials_rbac.py
@@ -0,0 +1,85 @@
+# Copyright 2018 AT&T Corporation.
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from tempest import config
+from tempest.lib.common.utils import data_utils
+from tempest.lib.common.utils import test_utils
+from tempest.lib import decorators
+
+from patrole_tempest_plugin import rbac_rule_validation
+from patrole_tempest_plugin.tests.api.identity import rbac_base
+
+
+CONF = config.CONF
+
+
+class ApplicationCredentialsV3RbacTest(rbac_base.BaseIdentityV3RbacTest):
+
+ @classmethod
+ def skip_checks(cls):
+ super(ApplicationCredentialsV3RbacTest, cls).skip_checks()
+ if not CONF.identity_feature_enabled.application_credentials:
+ raise cls.skipException("Application credentials are not available"
+ " in this environment")
+
+ @classmethod
+ def resource_setup(cls):
+ super(ApplicationCredentialsV3RbacTest, cls).resource_setup()
+ cls.user_id = cls.os_primary.credentials.user_id
+
+ def _create_application_credential(self, name=None, **kwargs):
+ name = name or data_utils.rand_name('application_credential')
+ application_credential = (
+ self.application_credentials_client.create_application_credential(
+ self.user_id, name=name, **kwargs))['application_credential']
+ self.addCleanup(
+ test_utils.call_and_ignore_notfound_exc,
+ self.application_credentials_client.delete_application_credential,
+ self.user_id,
+ application_credential['id'])
+ return application_credential
+
+ @decorators.idempotent_id('b53bee14-e9df-4929-b257-6def76c12e4d')
+ @rbac_rule_validation.action(service="keystone",
+ rule="identity:create_application_credential")
+ def test_create_application_credential(self):
+ with self.rbac_utils.override_role(self):
+ self._create_application_credential()
+
+ @decorators.idempotent_id('58b3c3a0-5ad0-44f7-8da7-0736f71f7168')
+ @rbac_rule_validation.action(service="keystone",
+ rule="identity:list_application_credentials")
+ def test_list_application_credentials(self):
+ with self.rbac_utils.override_role(self):
+ self.application_credentials_client.list_application_credentials(
+ user_id=self.user_id)
+
+ @decorators.idempotent_id('d7b13968-a8a6-47fd-8e1d-7cc7f565c7f8')
+ @rbac_rule_validation.action(service="keystone",
+ rule="identity:get_application_credential")
+ def test_show_application_credential(self):
+ app_cred = self._create_application_credential()
+ with self.rbac_utils.override_role(self):
+ self.application_credentials_client.show_application_credential(
+ user_id=self.user_id, application_credential_id=app_cred['id'])
+
+ @decorators.idempotent_id('521b7c0f-1dd5-47a6-ae95-95c0323d7735')
+ @rbac_rule_validation.action(service="keystone",
+ rule="identity:delete_application_credential")
+ def test_delete_application_credential(self):
+ app_cred = self._create_application_credential()
+ with self.rbac_utils.override_role(self):
+ self.application_credentials_client.delete_application_credential(
+ user_id=self.user_id, application_credential_id=app_cred['id'])
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 84ce2c7..932683d 100644
--- a/patrole_tempest_plugin/tests/api/network/test_networks_rbac.py
+++ b/patrole_tempest_plugin/tests/api/network/test_networks_rbac.py
@@ -294,63 +294,6 @@
with self.rbac_utils.override_role(self):
self.networks_client.delete_network(network['id'])
- @rbac_rule_validation.action(service="neutron",
- rule="create_subnet")
- @decorators.idempotent_id('44f42aaf-8a9a-4678-868a-b8fe82689554')
- def test_create_subnet(self):
-
- """Create Subnet Test
-
- RBAC test for the neutron create_subnet policy
- """
- network = self._create_network()
-
- with self.rbac_utils.override_role(self):
- self.create_subnet(network, enable_dhcp=False)
-
- @rbac_rule_validation.action(service="neutron",
- rule="get_subnet")
- @decorators.idempotent_id('eb88be84-2465-482b-a40b-5201acb41152')
- def test_show_subnet(self):
-
- """Show Subnet Test
-
- RBAC test for the neutron get_subnet policy
- """
- with self.rbac_utils.override_role(self):
- self.subnets_client.show_subnet(self.subnet['id'])
-
- @rbac_rule_validation.action(service="neutron",
- rule="update_subnet")
- @decorators.idempotent_id('1bfeaec5-83b9-4140-8138-93a0a9d04cee')
- def test_update_subnet(self):
-
- """Update Subnet Test
-
- RBAC test for the neutron update_subnet policy
- """
- updated_name = data_utils.rand_name(
- self.__class__.__name__ + '-Network')
-
- with self.rbac_utils.override_role(self):
- self.subnets_client.update_subnet(self.subnet['id'],
- name=updated_name)
-
- @rbac_rule_validation.action(service="neutron",
- rule="delete_subnet")
- @decorators.idempotent_id('1ad1400f-dc84-4edb-9674-b33bbfb0d3e3')
- def test_delete_subnet(self):
-
- """Delete Subnet Test
-
- RBAC test for the neutron delete_subnet policy
- """
- network = self._create_network()
- subnet = self.create_subnet(network, enable_dhcp=False)
-
- with self.rbac_utils.override_role(self):
- self.subnets_client.delete_subnet(subnet['id'])
-
@utils.requires_ext(extension='dhcp_agent_scheduler', service='network')
@decorators.idempotent_id('b524f19f-fbb4-4d11-a85d-03bfae17bf0e')
@rbac_rule_validation.action(service="neutron",
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 ab85745..812b0c1 100644
--- a/patrole_tempest_plugin/tests/api/network/test_routers_rbac.py
+++ b/patrole_tempest_plugin/tests/api/network/test_routers_rbac.py
@@ -330,7 +330,8 @@
self.routers_client.delete_router(router['id'])
@rbac_rule_validation.action(service="neutron",
- rule="add_router_interface")
+ rules=["get_router", "add_router_interface"],
+ expected_error_codes=[404, 403])
@decorators.idempotent_id('a0627778-d68d-4913-881b-e345360cca19')
def test_add_router_interface(self):
"""Add Router Interface
diff --git a/patrole_tempest_plugin/tests/api/volume/test_volume_actions_rbac.py b/patrole_tempest_plugin/tests/api/volume/test_volume_actions_rbac.py
index 013361a..a4fc3fd 100644
--- a/patrole_tempest_plugin/tests/api/volume/test_volume_actions_rbac.py
+++ b/patrole_tempest_plugin/tests/api/volume/test_volume_actions_rbac.py
@@ -73,6 +73,7 @@
'"volume_extension:volume_actions:attach" must be available in the '
'cloud.')
@utils.services('compute')
+ @decorators.attr(type='slow')
@rbac_rule_validation.action(
service="cinder",
rule="volume_extension:volume_actions:attach")
@@ -115,16 +116,6 @@
self.addCleanup(self.volumes_client.update_volume_readonly,
self.volume['id'], readonly=False)
- @decorators.idempotent_id('72bab13c-dfaf-4b6d-a132-c83a85fb1776')
- @rbac_rule_validation.action(
- service="cinder",
- rule="volume_extension:volume_unmanage")
- def test_unmanage_volume(self):
- volume = self.create_volume()
-
- with self.rbac_utils.override_role(self):
- self.volumes_client.unmanage_volume(volume['id'])
-
@decorators.idempotent_id('59b783c0-f4ef-430c-8a90-1bad97d4ec5c')
@rbac_rule_validation.action(service="cinder",
rule="volume:update")
diff --git a/patrole_tempest_plugin/tests/api/volume/test_volume_basic_crud_rbac.py b/patrole_tempest_plugin/tests/api/volume/test_volume_basic_crud_rbac.py
index 1bd87d2..61532c6 100644
--- a/patrole_tempest_plugin/tests/api/volume/test_volume_basic_crud_rbac.py
+++ b/patrole_tempest_plugin/tests/api/volume/test_volume_basic_crud_rbac.py
@@ -62,11 +62,3 @@
with self.rbac_utils.override_role(self):
self.volumes_client.update_volume(self.volume['id'],
name=update_name)
-
- @rbac_rule_validation.action(
- service="cinder",
- rule="volume_extension:volume_image_metadata")
- @decorators.idempotent_id('3d48ca91-f02b-4616-a69d-4a8b296c8529')
- def test_volume_list_image_metadata(self):
- with self.rbac_utils.override_role(self):
- self.volumes_client.list_volumes(detail=True)
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 85547e1..2ae860c 100644
--- a/patrole_tempest_plugin/tests/unit/test_rbac_rule_validation.py
+++ b/patrole_tempest_plugin/tests/unit/test_rbac_rule_validation.py
@@ -13,6 +13,7 @@
# under the License.
import mock
+from oslo_config import cfg
from tempest.lib import exceptions
from tempest import manager
@@ -24,11 +25,13 @@
from patrole_tempest_plugin import rbac_utils
from patrole_tempest_plugin.tests.unit import fixtures
+CONF = cfg.CONF
-class RBACRuleValidationTest(base.TestCase):
+
+class BaseRBACRuleValidationTest(base.TestCase):
def setUp(self):
- super(RBACRuleValidationTest, self).setUp()
+ super(BaseRBACRuleValidationTest, 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(
@@ -45,6 +48,12 @@
self.useFixture(
fixtures.ConfPatcher(enable_reporting=False, group='patrole_log'))
+
+class RBACRuleValidationTest(BaseRBACRuleValidationTest):
+ """Test suite for validating fundamental functionality for the
+ ``rbac_rule_validation`` decorator.
+ """
+
@mock.patch.object(rbac_rv, 'LOG', autospec=True)
@mock.patch.object(rbac_rv, 'policy_authority', autospec=True)
def test_rule_validation_have_permission_no_exc(self, mock_authority,
@@ -98,11 +107,11 @@
def test_policy(*args):
raise exceptions.Forbidden()
- test_re = "Role Member was not allowed to perform sentinel.action."
+ test_re = ("Role Member was not allowed to perform the following "
+ "actions: \[%s\].*" % (mock.sentinel.action))
self.assertRaisesRegex(exceptions.Forbidden, test_re, test_policy,
self.mock_test_args)
- mock_log.error.assert_called_once_with("Role Member was not allowed to"
- " perform sentinel.action.")
+ 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)
@@ -138,11 +147,11 @@
def test_policy(*args):
raise rbac_exceptions.RbacMalformedResponse()
- test_re = "Role Member was not allowed to perform sentinel.action."
+ test_re = ("Role Member was not allowed to perform the following "
+ "actions: \[%s\].*" % (mock.sentinel.action))
self.assertRaisesRegex(exceptions.Forbidden, test_re, test_policy,
self.mock_test_args)
- mock_log.error.assert_called_once_with("Role Member was not allowed to"
- " perform sentinel.action.")
+ 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)
@@ -179,11 +188,11 @@
def test_policy(*args):
raise rbac_exceptions.RbacConflictingPolicies()
- test_re = "Role Member was not allowed to perform sentinel.action."
+ test_re = ("Role Member was not allowed to perform the following "
+ "actions: \[%s\].*" % (mock.sentinel.action))
self.assertRaisesRegex(exceptions.Forbidden, test_re, test_policy,
self.mock_test_args)
- mock_log.error.assert_called_once_with("Role Member was not allowed to"
- " perform sentinel.action.")
+ 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)
@@ -233,25 +242,26 @@
raise exceptions.NotFound()
expected_errors = [
- "Role Member was not allowed to perform sentinel.action.", None
+ ("Role Member was not allowed to perform the following "
+ "actions: \[%s\].*" % (mock.sentinel.action)),
+ None
]
for pos, allowed in enumerate([True, False]):
mock_authority.PolicyAuthority.return_value.allowed\
.return_value = allowed
- expected_error = expected_errors[pos]
+ error_re = expected_errors[pos]
- if expected_error:
- self.assertRaisesRegex(
- exceptions.Forbidden, '.* ' + expected_error, test_policy,
- self.mock_test_args)
- mock_log.error.assert_called_once_with(expected_error)
+ if error_re:
+ self.assertRaisesRegex(exceptions.Forbidden, 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_once_with(
+ mock_log.warning.assert_called_with(
"NotFound exception was caught for policy action {0}. The "
"service {1} throws a 404 instead of a 403, which is "
"irregular.".format(mock.sentinel.action,
@@ -284,13 +294,10 @@
for test_policy in (
test_policy_expect_forbidden, test_policy_expect_not_found):
- error_re = (".* OverPermission: Role Member was allowed to perform"
- " sentinel.action")
+ error_re = ".*OverPermission: .* \[%s\]$" % mock.sentinel.action
self.assertRaisesRegex(rbac_exceptions.RbacOverPermission,
error_re, test_policy, self.mock_test_args)
- mock_log.error.assert_called_once_with(
- 'Role %s was allowed to perform %s', 'Member',
- mock.sentinel.action)
+ self.assertRegex(mock_log.error.mock_calls[0][1][0], error_re)
mock_log.error.reset_mock()
@mock.patch.object(rbac_rv, 'policy_authority', autospec=True)
@@ -405,3 +412,264 @@
mock.sentinel.action,
"Allowed",
"Allowed")
+
+
+class RBACRuleValidationTestMultiPolicy(BaseRBACRuleValidationTest):
+ """Test suite for validating multi-policy support for the
+ ``rbac_rule_validation`` decorator.
+ """
+
+ 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.patch.object(rbac_rv, 'policy_authority', autospec=True)
+ def test_rule_validation_multi_policy_have_permission_success(
+ self, mock_authority):
+ """Test that when expected result is authorized and test passes that
+ the overall evaluation succeeds.
+ """
+ mock_authority.PolicyAuthority.return_value.allowed.\
+ return_value = True
+
+ rules = [mock.sentinel.action1, mock.sentinel.action2]
+
+ @rbac_rv.action(mock.sentinel.service, rules=rules,
+ expected_error_codes=[403, 403])
+ def test_policy(*args):
+ pass
+
+ test_policy(self.mock_test_args)
+ self._assert_policy_authority_called_with(rules, mock_authority)
+
+ @mock.patch.object(rbac_rv, 'LOG', autospec=True)
+ @mock.patch.object(rbac_rv, 'policy_authority', autospec=True)
+ def test_rule_validation_multi_policy_overpermission_failure(
+ self, mock_authority, mock_log):
+ """Test that when expected result is unauthorized and test passes that
+ the overall evaluation results in an OverPermission getting raised.
+ """
+
+ rules = [
+ mock.sentinel.action1, mock.sentinel.action2, mock.sentinel.action3
+ ]
+ exp_ecodes = [403, 403, 403]
+
+ @rbac_rv.action(mock.sentinel.service, rules=rules,
+ expected_error_codes=exp_ecodes)
+ def test_policy(*args):
+ pass
+
+ def _do_test(allowed_list, fail_on_action):
+ mock_authority.PolicyAuthority.return_value.allowed.side_effect = (
+ allowed_list)
+
+ error_re = ".*OverPermission: .* \[%s\]$" % fail_on_action
+ self.assertRaisesRegex(rbac_exceptions.RbacOverPermission,
+ error_re, test_policy, self.mock_test_args)
+ mock_log.debug.assert_any_call(
+ "%s: Expecting %d to be raised for policy name: %s",
+ 'test_policy', 403, fail_on_action)
+ self.assertRegex(mock_log.error.mock_calls[0][1][0], error_re)
+ mock_log.error.reset_mock()
+ self._assert_policy_authority_called_with(rules, mock_authority)
+
+ _do_test([True, True, False], mock.sentinel.action3)
+ _do_test([False, True, True], mock.sentinel.action1)
+ _do_test([True, False, True], mock.sentinel.action2)
+
+ @mock.patch.object(rbac_rv, 'LOG', autospec=True)
+ @mock.patch.object(rbac_rv, 'policy_authority', autospec=True)
+ def test_rule_validation_multi_policy_forbidden_success(
+ self, mock_authority, mock_log):
+ """Test that when the expected result is unauthorized and the test
+ fails that the overall evaluation results in success.
+ """
+
+ rules = [
+ mock.sentinel.action1, mock.sentinel.action2, mock.sentinel.action3
+ ]
+ exp_ecodes = [403, 403, 403]
+
+ @rbac_rv.action(mock.sentinel.service, rules=rules,
+ expected_error_codes=exp_ecodes)
+ def test_policy(*args):
+ raise exceptions.Forbidden()
+
+ def _do_test(allowed_list, fail_on_action):
+ mock_authority.PolicyAuthority.return_value.allowed.\
+ side_effect = allowed_list
+ test_policy(self.mock_test_args)
+ mock_log.debug.assert_called_with(
+ "%s: Expecting %d to be raised for policy name: %s",
+ 'test_policy', 403, fail_on_action)
+ mock_log.error.assert_not_called()
+ self._assert_policy_authority_called_with(rules, mock_authority)
+
+ _do_test([True, True, False], mock.sentinel.action3)
+ _do_test([False, True, True], mock.sentinel.action1)
+ _do_test([True, False, True], mock.sentinel.action2)
+
+ @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 a Forbidden 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 = ("Role Member was not allowed to perform the following "
+ "actions: %s. Expected allowed actions: %s. Expected "
+ "disallowed actions: []." % (rules, rules)).replace(
+ '[', '\[').replace(']', '\]')
+ self.assertRaisesRegex(exceptions.Forbidden, 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)
+
+ @mock.patch.object(rbac_rv, 'LOG', autospec=True)
+ @mock.patch.object(rbac_rv, 'policy_authority', autospec=True)
+ def test_rule_validation_multi_actions_forbidden(
+ self, mock_authority, mock_log):
+ """Test that when the expected result is forbidden because
+ two of the actions fail and the first action specifies 403,
+ verify that the overall evaluation results in success.
+ """
+
+ rules = [
+ mock.sentinel.action1, mock.sentinel.action2, mock.sentinel.action3
+ ]
+ exp_ecodes = [403, 403, 404]
+
+ @rbac_rv.action(mock.sentinel.service, rules=rules,
+ expected_error_codes=exp_ecodes)
+ def test_policy(*args):
+ raise exceptions.Forbidden()
+
+ def _do_test(allowed_list, fail_on_action):
+ mock_authority.PolicyAuthority.return_value.allowed.\
+ side_effect = allowed_list
+ test_policy(self.mock_test_args)
+ mock_log.debug.assert_called_with(
+ "%s: Expecting %d to be raised for policy name: %s",
+ 'test_policy', 403, fail_on_action)
+ mock_log.error.assert_not_called()
+ self._assert_policy_authority_called_with(rules, mock_authority)
+
+ _do_test([False, True, False], mock.sentinel.action1)
+ _do_test([False, False, True], mock.sentinel.action1)
+
+ @mock.patch.object(rbac_rv, 'LOG', autospec=True)
+ @mock.patch.object(rbac_rv, 'policy_authority', autospec=True)
+ def test_rule_validation_multi_actions_notfound(
+ self, mock_authority, mock_log):
+ """Test that when the expected result is not found because
+ two of the actions fail and the first action specifies 404,
+ verify that the overall evaluation results in success.
+ """
+
+ rules = [
+ mock.sentinel.action1, mock.sentinel.action2,
+ mock.sentinel.action3, mock.sentinel.action4
+ ]
+ exp_ecodes = [403, 404, 403, 403]
+
+ @rbac_rv.action(mock.sentinel.service, rules=rules,
+ expected_error_codes=exp_ecodes)
+ def test_policy(*args):
+ raise exceptions.NotFound()
+
+ def _do_test(allowed_list, fail_on_action):
+ mock_authority.PolicyAuthority.return_value.allowed.\
+ side_effect = allowed_list
+ test_policy(self.mock_test_args)
+ mock_log.debug.assert_called_with(
+ "%s: Expecting %d to be raised for policy name: %s",
+ 'test_policy', 404, fail_on_action)
+ mock_log.error.assert_not_called()
+ self._assert_policy_authority_called_with(rules, mock_authority)
+
+ _do_test([True, False, False, True], mock.sentinel.action2)
+ _do_test([True, False, True, False], mock.sentinel.action2)
+
+ @mock.patch.object(rbac_rv, 'LOG', autospec=True)
+ def test_prepare_multi_policy_allowed_usages(self, mock_log):
+
+ def _do_test(rule, rules, ecode, ecodes, exp_rules, exp_ecodes):
+ rule_list, ec_list = rbac_rv._prepare_multi_policy(rule, rules,
+ ecode, ecodes)
+ self.assertEqual(rule_list, exp_rules)
+ self.assertEqual(ec_list, exp_ecodes)
+
+ # Validate that using deprecated values: rule and expected_error_code
+ # are converted into rules = [rule] and expected_error_codes =
+ # [expected_error_code]
+ _do_test("rule1", None, 403, None, ["rule1"], [403])
+
+ # Validate that rules = [rule] and expected_error_codes defaults to
+ # 403 when no values are provided.
+ _do_test("rule1", None, None, None, ["rule1"], [403])
+
+ # Validate that `len(rules) == len(expected_error_codes)` works when
+ # both == 1.
+ _do_test(None, ["rule1"], None, [403], ["rule1"], [403])
+
+ # Validate that `len(rules) == len(expected_error_codes)` works when
+ # both are > 1.
+ _do_test(None, ["rule1", "rule2"], None, [403, 404],
+ ["rule1", "rule2"], [403, 404])
+
+ # Validate that when only a default expected_error_code argument is
+ # provided, that default value and other default values (403) are
+ # filled into the expected_error_codes list.
+ # Example:
+ # @rbac_rv.action(service, rules=[<rule>, <rule>])
+ # def test_policy(*args):
+ # ...
+ _do_test(None, ["rule1", "rule2"], 403, None,
+ ["rule1", "rule2"], [403, 403])
+
+ # Validate that the deprecated values are ignored when new values are
+ # provided.
+ _do_test("rule3", ["rule1", "rule2"], 404, [403, 403],
+ ["rule1", "rule2"], [403, 403])
+ mock_log.debug.assert_any_call(
+ "The `rules` argument will be used instead of `rule`.")
+ mock_log.debug.assert_any_call(
+ "The `exp_error_codes` argument will be used instead of "
+ "`exp_error_code`.")
+
+ @mock.patch.object(rbac_rv, 'LOG', autospec=True)
+ def test_prepare_multi_policy_disallowed_usages(self, mock_log):
+
+ def _do_test(rule, rules, ecode, ecodes):
+ rule_list, ec_list = rbac_rv._prepare_multi_policy(rule, rules,
+ ecode, ecodes)
+
+ error_re = ("The `expected_error_codes` list is not the same length"
+ " as the `rules` list.")
+ # When len(rules) > 1 then len(expected_error_codes) must be same len.
+ self.assertRaisesRegex(ValueError, error_re, _do_test,
+ None, ["rule1", "rule2"], None, [403])
+ # When len(expected_error_codes) > 1 len(rules) must be same len.
+ self.assertRaisesRegex(ValueError, error_re, _do_test,
+ None, ["rule1"], None, [403, 404])
+ error_re = ("The `rules` list must be provided if using the "
+ "`expected_error_codes` list.")
+ # When expected_error_codes is provided rules must be as well.
+ self.assertRaisesRegex(ValueError, error_re, _do_test,
+ None, None, None, [404])
diff --git a/patrole_tempest_plugin/tests/unit/test_rbac_utils.py b/patrole_tempest_plugin/tests/unit/test_rbac_utils.py
index 5e730d3..4937318 100644
--- a/patrole_tempest_plugin/tests/unit/test_rbac_utils.py
+++ b/patrole_tempest_plugin/tests/unit/test_rbac_utils.py
@@ -36,17 +36,15 @@
def test_override_role_with_missing_admin_role(self):
self.rbac_utils.set_roles('member')
- error_re = (
- 'Roles defined by `\[patrole\] rbac_test_role` and `\[identity\] '
- 'admin_role` must be defined in the system.')
+ error_re = (".*Following roles were not found: admin. Available "
+ "roles: member.")
self.assertRaisesRegex(rbac_exceptions.RbacResourceSetupFailed,
error_re, self.rbac_utils.override_role)
def test_override_role_with_missing_rbac_role(self):
self.rbac_utils.set_roles('admin')
- error_re = (
- 'Roles defined by `\[patrole\] rbac_test_role` and `\[identity\] '
- 'admin_role` must be defined in the system.')
+ error_re = (".*Following roles were not found: member. Available "
+ "roles: admin.")
self.assertRaisesRegex(rbac_exceptions.RbacResourceSetupFailed,
error_re, self.rbac_utils.override_role)
diff --git a/releasenotes/notes/multi-policy-support-4e5c8b4e9e25ad9d.yaml b/releasenotes/notes/multi-policy-support-4e5c8b4e9e25ad9d.yaml
new file mode 100644
index 0000000..1f33d8f
--- /dev/null
+++ b/releasenotes/notes/multi-policy-support-4e5c8b4e9e25ad9d.yaml
@@ -0,0 +1,31 @@
+---
+features:
+ - |
+ Patrole now offers support for multiple policies. The ``rules`` argument
+ has been added to the ``rbac_rule_validation.action`` decorator, which
+ takes a list of policy names which Patrole will use to determine the
+ expected test result. This allows Patrole to more accurately determine
+ whether RBAC is configured correctly, since some API endpoints enforce
+ multiple policies.
+
+ Multiple policy support includes the capability to specify multiple
+ expected error codes, as some components may return different error codes
+ for different roles due to checking multiple policy rules. The
+ ``expected_error_codes`` argument has been added to the
+ ``rbac_rule_validation.action`` decorator, which is a list of error codes
+ expected when the corresponding rule in the ``rules`` list is disallowed
+ to perform the API action. For this reason, the error codes in the
+ ``expected_error_codes`` list must appear in the same order as their
+ corresponding rules in the ``rules`` list. For example:
+
+ expected_error_codes[0] is the error code for the rules[0] rule.
+ expected_error_codes[1] is the error code for the rules[1] rule.
+ ...
+
+deprecations:
+ - |
+ The ``rule`` argument in the ``rbac_rule_validation.action`` decorator has
+ been deprecated in favor of ``rules``.
+
+ The ``expected_error_code`` argument in the ``rbac_rule_validation.action``
+ decorator has been deprecated in favor of ``expected_error_codes``.
diff --git a/tox.ini b/tox.ini
index cca09d0..a09822f 100644
--- a/tox.ini
+++ b/tox.ini
@@ -21,16 +21,20 @@
stestr --test-path ./patrole_tempest_plugin/tests/unit run {posargs}
[testenv:pep8]
+basepython = python3
commands = flake8 {posargs}
check-uuid --package patrole_tempest_plugin.tests.api
[testenv:uuidgen]
+basepython = python3
commands = check-uuid --package patrole_tempest_plugin.tests.api --fix
[testenv:venv]
+basepython = python3
commands = {posargs}
[testenv:cover]
+basepython = python3
commands = rm -rf *.pyc
rm -rf cover
rm -f .coverage
@@ -46,6 +50,7 @@
rm
[testenv:docs]
+basepython = python3
deps =
-c{env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt}
-r{toxinidir}/requirements.txt
@@ -56,6 +61,7 @@
whitelist_externals = rm
[testenv:releasenotes]
+basepython = python3
deps =
-c{env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt}
-r{toxinidir}/requirements.txt
@@ -66,9 +72,11 @@
whitelist_externals = rm
[testenv:debug]
+basepython = python3
commands = oslo_debug_helper -t patrole_tempest_plugin/tests {posargs}
[testenv:genconfig]
+basepython = python3
commands = oslo-config-generator --config-file etc/config-generator.patrole.conf
[flake8]