Merge "Fix flavor_rxtx_rbac"
diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst
deleted file mode 100644
index 243a9c0..0000000
--- a/CONTRIBUTING.rst
+++ /dev/null
@@ -1,17 +0,0 @@
-If you would like to contribute to the development of OpenStack, you must
-follow the steps in this page:
-
- https://docs.openstack.org/infra/manual/developers.html
-
-If you already have a good understanding of how the system works and your
-OpenStack accounts are set up, you can skip to the development workflow
-section of this documentation to learn how changes to OpenStack should be
-submitted for review via the Gerrit tool:
-
- https://docs.openstack.org/infra/manual/developers.html#development-workflow
-
-Pull requests submitted through GitHub will be ignored.
-
-Bugs should be filed on Launchpad, not GitHub:
-
- https://bugs.launchpad.net/patrole
diff --git a/HACKING.rst b/HACKING.rst
index 3bc8270..a94b47c 100644
--- a/HACKING.rst
+++ b/HACKING.rst
@@ -46,7 +46,7 @@
test does not call ``rbac_utils.switch_role`` with ``toggle_rbac_role=True``
within the RBAC test, then the test is *not* a valid RBAC test: The API
endpoint under test will be performed with admin credentials, which is always
-wrong unless ``CONF.rbac_test_role`` is admin.
+wrong unless ``CONF.patrole.rbac_test_role`` is admin.
.. note::
diff --git a/doc/source/HACKING.rst b/doc/source/HACKING.rst
new file mode 100644
index 0000000..1847447
--- /dev/null
+++ b/doc/source/HACKING.rst
@@ -0,0 +1,4 @@
+=======
+Hacking
+=======
+.. include:: ../../HACKING.rst
diff --git a/doc/source/contributing.rst b/doc/source/contributing.rst
deleted file mode 100644
index 1728a61..0000000
--- a/doc/source/contributing.rst
+++ /dev/null
@@ -1,4 +0,0 @@
-============
-Contributing
-============
-.. include:: ../../CONTRIBUTING.rst
diff --git a/doc/source/index.rst b/doc/source/index.rst
index f58ee7f..e2cc0bd 100644
--- a/doc/source/index.rst
+++ b/doc/source/index.rst
@@ -20,9 +20,17 @@
=================
.. toctree::
+ :maxdepth: 1
+
+ HACKING
+
+Framework
+---------
+
+.. toctree::
:maxdepth: 2
- contributing
+ rbac_validation
Indices and tables
==================
diff --git a/doc/source/rbac_validation.rst b/doc/source/rbac_validation.rst
new file mode 100644
index 0000000..ccaf3c8
--- /dev/null
+++ b/doc/source/rbac_validation.rst
@@ -0,0 +1,64 @@
+.. _rbac-validation:
+
+RBAC Testing Validation
+=======================
+
+--------
+Overview
+--------
+
+RBAC Testing Validation is broken up into 3 stages:
+
+ 1. "Expected" stage. Determine whether the test should be able to succeed
+ or fail based on the test role defined by ``[patrole] rbac_test_role``)
+ and the policy action that the test enforces.
+ 2. "Actual" stage. Run the test by calling the API endpoint that enforces
+ the expected policy action using the test role.
+ 3. 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:
+
+ * Expected result is ``True`` and the test passes.
+ * Expected result is ``False`` and the test fails.
+
+ "Inconsistent" (or failing) cases include:
+
+ * Expected result is ``False`` and the test passes. This results in an
+ ``RbacOverPermission`` exception getting thrown.
+ * Expected result is ``True`` and the test fails. This results in a
+ ``Forbidden`` exception getting thrown.
+
+ For example, a 200 from the API call and a ``True`` result from
+ ``oslo.policy`` or a 403 from the API call and a ``False`` result from
+ ``oslo.policy`` are successful results.
+
+-------------------------------
+The RBAC Rule Validation Module
+-------------------------------
+
+High-level module that implements decorator inside which the "Expected" stage
+is initiated.
+
+.. automodule:: patrole_tempest_plugin.rbac_rule_validation
+ :members:
+
+---------------------------
+The Policy Authority Module
+---------------------------
+
+Using the Policy Authority Module, policy verification is performed by:
+
+1. Pooling together the default `in-code` policy rules.
+2. Overriding the defaults with custom policy rules located in a policy.json,
+ if the policy file exists and the custom policy definition is explicitly
+ defined therein.
+3. Confirming that the policy action -- for example, "list_users" -- exists.
+ (``oslo.policy`` otherwise claims that role "foo" is allowed to
+ perform policy action "bar", for example, because it defers to the
+ "default" policy rule and oftentimes the default can be "anyone allowed").
+4. Performing a call with all necessary data to ``oslo.policy`` and returning
+ the expected result back to ``rbac_rule_validation`` decorator.
+
+.. automodule:: patrole_tempest_plugin.policy_authority
+ :members:
+ :special-members:
diff --git a/patrole_tempest_plugin/config.py b/patrole_tempest_plugin/config.py
index a53edd4..fcf29af 100644
--- a/patrole_tempest_plugin/config.py
+++ b/patrole_tempest_plugin/config.py
@@ -147,3 +147,23 @@
help="This group is deprecated and will be removed "
"in the next release. Use the [patrole] group "
"instead.")
+
+patrole_log_group = cfg.OptGroup(
+ name='patrole_log', title='Patrole Logging Options')
+
+PatroleLogGroup = [
+ cfg.BoolOpt('enable_reporting',
+ default=False,
+ help="Enables reporting on RBAC expected and actual test "
+ "results for each Patrole test"),
+ cfg.StrOpt('report_log_name',
+ default='patrole.log',
+ help="Name of file where output from 'enable_reporting' is "
+ "logged. Note that this file is recreated on each "
+ "invocation of patrole"),
+ cfg.StrOpt('report_log_path',
+ default='.',
+ help="Path (relative or absolute) where the output from "
+ "'enable_reporting' is logged. This is combined with"
+ "report_log_name to generate the full path."),
+]
diff --git a/patrole_tempest_plugin/plugin.py b/patrole_tempest_plugin/plugin.py
index 4bba037..b7717ea 100644
--- a/patrole_tempest_plugin/plugin.py
+++ b/patrole_tempest_plugin/plugin.py
@@ -13,15 +13,21 @@
# License for the specific language governing permissions and limitations
# under the License.
+import logging
import os
+from oslo_concurrency import lockutils
+
from tempest import config
from tempest.test_discover import plugins
from patrole_tempest_plugin import config as project_config
+RBACLOG = logging.getLogger('rbac_reporting')
+
class PatroleTempestPlugin(plugins.TempestPlugin):
+
def load_tests(self):
base_path = os.path.split(os.path.dirname(
os.path.abspath(__file__)))[0]
@@ -29,6 +35,32 @@
full_test_dir = os.path.join(base_path, test_dir)
return full_test_dir, base_path
+ @lockutils.synchronized('_reset_log_file')
+ def _reset_log_file(self, logfile):
+ try:
+ os.remove(logfile)
+ except OSError:
+ pass
+
+ def _configure_per_test_logging(self, conf):
+ # Separate log handler for rbac reporting
+ RBACLOG.setLevel(level=logging.INFO)
+ # Set up proper directory handling
+ report_abs_path = os.path.abspath(conf.patrole_log.report_log_path)
+ report_path = os.path.join(
+ report_abs_path, conf.patrole_log.report_log_name)
+
+ # Remove the log file if it exists
+ self._reset_log_file(report_path)
+
+ # Delay=True so that we don't end up creating an empty file if we
+ # never log to it.
+ rbac_report_handler = logging.FileHandler(
+ filename=report_path, delay=True, mode='a')
+ rbac_report_handler.setFormatter(
+ fmt=logging.Formatter(fmt='%(message)s'))
+ RBACLOG.addHandler(rbac_report_handler)
+
def register_opts(self, conf):
# TODO(fmontei): Remove ``rbac_group`` in a future release as it is
# currently deprecated.
@@ -40,6 +72,13 @@
conf,
project_config.patrole_group,
project_config.PatroleGroup)
+ config.register_opt_group(
+ conf,
+ project_config.patrole_log_group,
+ project_config.PatroleLogGroup)
+
+ if conf.patrole_log.enable_reporting:
+ self._configure_per_test_logging(conf)
def get_opt_lists(self):
return [(project_config.patrole_group.name,
diff --git a/patrole_tempest_plugin/rbac_policy_parser.py b/patrole_tempest_plugin/policy_authority.py
similarity index 99%
rename from patrole_tempest_plugin/rbac_policy_parser.py
rename to patrole_tempest_plugin/policy_authority.py
index aff4e66..af227c4 100644
--- a/patrole_tempest_plugin/rbac_policy_parser.py
+++ b/patrole_tempest_plugin/policy_authority.py
@@ -31,7 +31,7 @@
LOG = logging.getLogger(__name__)
-class RbacPolicyParser(RbacAuthority):
+class PolicyAuthority(RbacAuthority):
"""A class for parsing policy rules into lists of allowed roles.
RBAC testing requires that each rule in a policy file be broken up into
diff --git a/patrole_tempest_plugin/rbac_rule_validation.py b/patrole_tempest_plugin/rbac_rule_validation.py
index d06986a..69274b3 100644
--- a/patrole_tempest_plugin/rbac_rule_validation.py
+++ b/patrole_tempest_plugin/rbac_rule_validation.py
@@ -23,8 +23,8 @@
from tempest.lib import exceptions
from tempest import test
+from patrole_tempest_plugin import policy_authority
from patrole_tempest_plugin import rbac_exceptions
-from patrole_tempest_plugin import rbac_policy_parser
from patrole_tempest_plugin import rbac_utils
from patrole_tempest_plugin import requirements_authority
@@ -33,6 +33,8 @@
_SUPPORTED_ERROR_CODES = [403, 404]
+RBACLOG = logging.getLogger('rbac_reporting')
+
def action(service, rule='', admin_only=False, expected_error_code=403,
extra_target_data=None):
@@ -118,7 +120,7 @@
if extra_target_data is None:
extra_target_data = {}
- def decorator(func):
+ def decorator(test_func):
role = CONF.patrole.rbac_test_role
def wrapper(*args, **kwargs):
@@ -129,28 +131,26 @@
'`rbac_rule_validation` decorator can only be applied to '
'an instance of `tempest.test.BaseTestCase`.')
- if admin_only:
- LOG.info("As admin_only is True, only admin role should be "
- "allowed to perform the API. Skipping oslo.policy "
- "check for policy action {0}.".format(rule))
- allowed = rbac_utils.is_admin()
- else:
- allowed = _is_authorized(test_obj, service, rule,
- extra_target_data)
+ allowed = _is_authorized(test_obj, service, rule,
+ extra_target_data, admin_only)
expected_exception, irregular_msg = _get_exception_type(
expected_error_code)
+ test_status = 'Allowed'
+
try:
- func(*args, **kwargs)
+ test_func(*args, **kwargs)
except rbac_exceptions.RbacInvalidService as e:
msg = ("%s is not a valid service." % service)
+ test_status = ('Error, %s' % (msg))
LOG.error(msg)
raise exceptions.NotFound(
"%s RbacInvalidService was: %s" % (msg, e))
except (expected_exception,
rbac_exceptions.RbacConflictingPolicies,
rbac_exceptions.RbacMalformedResponse) as e:
+ test_status = 'Denied'
if irregular_msg:
LOG.warning(irregular_msg.format(rule, service))
if allowed:
@@ -162,9 +162,10 @@
except Exception as e:
exc_info = sys.exc_info()
error_details = exc_info[1].__str__()
- msg = ("%s An unexpected exception has occurred: Expected "
- "exception was %s, which was not thrown."
- % (error_details, expected_exception.__name__))
+ msg = ("An unexpected exception has occurred during test: %s, "
+ "Exception was: %s"
+ % (test_func.__name__, error_details))
+ test_status = ('Error, %s' % (error_details))
LOG.error(msg)
six.reraise(exc_info[0], exc_info[0](msg), exc_info[2])
else:
@@ -177,13 +178,20 @@
finally:
test_obj.rbac_utils.switch_role(test_obj,
toggle_rbac_role=False)
+ if CONF.patrole_log.enable_reporting:
+ RBACLOG.info(
+ "[Service]: %s, [Test]: %s, [Rule]: %s, "
+ "[Expected]: %s, [Actual]: %s",
+ service, test_func.__name__, rule,
+ "Allowed" if allowed else "Denied",
+ test_status)
_wrapper = testtools.testcase.attr(role)(wrapper)
return _wrapper
return decorator
-def _is_authorized(test_obj, service, rule, extra_target_data):
+def _is_authorized(test_obj, service, rule, extra_target_data, admin_only):
"""Validates whether current RBAC role has permission to do policy action.
:param test_obj: An instance or subclass of `tempest.base.BaseTestCase`.
@@ -195,8 +203,15 @@
`tempest.base.BaseTestCase` attributes. Used by `oslo.policy` for
performing matching against attributes that are sent along with the API
calls.
+ :param admin_only: Skips over `oslo.policy` check because the policy action
+ defined by `rule` is not enforced by the service's policy
+ enforcement engine. For example, Keystone v2 performs an admin check
+ for most of its endpoints. If True, `rule` is effectively
+ ignored.
+
:returns: True if the current RBAC role can perform the policy action,
else False.
+
:raises RbacResourceSetupFailed: If `project_id` or `user_id` are missing
from the `auth_provider` attribute in `test_obj`.
:raises RbacParsingException: if ``[patrole] strict_policy_check`` is True
@@ -204,6 +219,13 @@
:raises skipException: If ``[patrole] strict_policy_check`` is False and
the ``rule`` does not exist in the system.
"""
+
+ if admin_only:
+ LOG.info("As admin_only is True, only admin role should be "
+ "allowed to perform the API. Skipping oslo.policy "
+ "check for policy action {0}.".format(rule))
+ return rbac_utils.is_admin()
+
try:
project_id = test_obj.os_primary.credentials.project_id
user_id = test_obj.os_primary.credentials.user_id
@@ -215,14 +237,14 @@
try:
role = CONF.patrole.rbac_test_role
- # Test RBAC against custom requirements. Otherwise use oslo.policy
+ # Test RBAC against custom requirements. Otherwise use oslo.policy.
if CONF.patrole.test_custom_requirements:
authority = requirements_authority.RequirementsAuthority(
CONF.patrole.custom_requirements_file, service)
else:
formatted_target_data = _format_extra_target_data(
test_obj, extra_target_data)
- authority = rbac_policy_parser.RbacPolicyParser(
+ authority = policy_authority.PolicyAuthority(
project_id, user_id, service,
extra_target_data=formatted_target_data)
is_allowed = authority.allowed(rule, role)
@@ -260,7 +282,7 @@
irregular_msg = None
if not isinstance(expected_error_code, six.integer_types) \
- or expected_error_code not in _SUPPORTED_ERROR_CODES:
+ or expected_error_code not in _SUPPORTED_ERROR_CODES:
msg = ("Please pass an expected error code. Currently "
"supported codes: {0}".format(_SUPPORTED_ERROR_CODES))
LOG.error(msg)
diff --git a/patrole_tempest_plugin/rbac_utils.py b/patrole_tempest_plugin/rbac_utils.py
index 5736645..81fefb2 100644
--- a/patrole_tempest_plugin/rbac_utils.py
+++ b/patrole_tempest_plugin/rbac_utils.py
@@ -221,9 +221,19 @@
@six.add_metaclass(abc.ABCMeta)
class RbacAuthority(object):
- # TODO(rb560u): Add documentation explaining what this class is for
+ """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_name, role):
- """Determine whether the role should be able to perform the API"""
- return
+ 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/tests/api/compute/rbac_base.py b/patrole_tempest_plugin/tests/api/compute/rbac_base.py
index 3807ae9..bab193e 100644
--- a/patrole_tempest_plugin/tests/api/compute/rbac_base.py
+++ b/patrole_tempest_plugin/tests/api/compute/rbac_base.py
@@ -53,9 +53,12 @@
for flavor in cls.flavors:
test_utils.call_and_ignore_notfound_exc(
cls.flavors_client.delete_flavor, flavor['id'])
+ for flavor in cls.flavors:
+ test_utils.call_and_ignore_notfound_exc(
+ cls.flavors_client.wait_for_resource_deletion, flavor['id'])
@classmethod
- def _create_flavor(cls, **kwargs):
+ def create_flavor(cls, **kwargs):
flavor_kwargs = {
"name": data_utils.rand_name(cls.__name__ + '-flavor'),
"ram": data_utils.rand_int_id(1, 10),
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 b196d93..26c9957 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
@@ -38,7 +38,7 @@
@classmethod
def resource_setup(cls):
super(FlavorAccessRbacTest, cls).resource_setup()
- cls.flavor_id = cls._create_flavor(is_public=False)['id']
+ cls.flavor_id = cls.create_flavor(is_public=False)['id']
cls.public_flavor_id = CONF.compute.flavor_ref
cls.tenant_id = cls.os_primary.credentials.tenant_id
diff --git a/patrole_tempest_plugin/tests/api/compute/test_flavor_extra_specs_rbac.py b/patrole_tempest_plugin/tests/api/compute/test_flavor_extra_specs_rbac.py
index e59fd78..031d8ad 100644
--- a/patrole_tempest_plugin/tests/api/compute/test_flavor_extra_specs_rbac.py
+++ b/patrole_tempest_plugin/tests/api/compute/test_flavor_extra_specs_rbac.py
@@ -34,7 +34,7 @@
@classmethod
def resource_setup(cls):
super(FlavorExtraSpecsRbacTest, cls).resource_setup()
- cls.flavor = cls._create_flavor()
+ cls.flavor = cls.create_flavor()
@classmethod
def resource_cleanup(cls):
diff --git a/patrole_tempest_plugin/tests/api/compute/test_flavor_manage_rbac.py b/patrole_tempest_plugin/tests/api/compute/test_flavor_manage_rbac.py
new file mode 100644
index 0000000..519a55a
--- /dev/null
+++ b/patrole_tempest_plugin/tests/api/compute/test_flavor_manage_rbac.py
@@ -0,0 +1,53 @@
+# 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.lib import decorators
+from tempest import test
+
+from patrole_tempest_plugin import rbac_rule_validation
+from patrole_tempest_plugin.tests.api.compute import rbac_base
+
+
+class FlavorManageRbacTest(rbac_base.BaseV2ComputeRbacTest):
+
+ # Need admin to wait for resource deletion below to avoid test role
+ # having to pass extra policies.
+ credentials = ['primary', 'admin']
+
+ @classmethod
+ def skip_checks(cls):
+ super(FlavorManageRbacTest, cls).skip_checks()
+ if not test.is_extension_enabled('OS-FLV-EXT-DATA', 'compute'):
+ msg = "OS-FLV-EXT-DATA extension not enabled."
+ raise cls.skipException(msg)
+
+ @decorators.idempotent_id('a4e7faec-7a4b-4809-9856-90d5b747ca35')
+ @rbac_rule_validation.action(
+ service="nova",
+ rule="os_compute_api:os-flavor-manage:create")
+ def test_create_flavor_manage(self):
+ self.rbac_utils.switch_role(self, toggle_rbac_role=True)
+ self.create_flavor()
+
+ @decorators.idempotent_id('782e988e-061b-4c40-896f-a77c70c2b057')
+ @rbac_rule_validation.action(
+ service="nova",
+ rule="os_compute_api:os-flavor-manage:delete")
+ def test_delete_flavor_manage(self):
+ flavor_id = self.create_flavor()['id']
+
+ self.rbac_utils.switch_role(self, toggle_rbac_role=True)
+ self.flavors_client.delete_flavor(flavor_id)
+ self.os_admin.flavors_client.wait_for_resource_deletion(flavor_id)
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 b64eef8..63dee63 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
@@ -42,6 +42,8 @@
Only applies to:
* policy "families" that require server creation
* small policy "families" -- i.e. containing one to three policies
+
+ Tests are ordered by policy name.
"""
credentials = ['primary', 'admin']
@@ -167,41 +169,36 @@
# Force-deleting a server enforces os-deferred-delete.
self.servers_client.force_delete_server(self.server['id'])
- @test.requires_ext(extension='os-instance-actions', service='compute')
- @decorators.idempotent_id('9d1b131d-407e-4fa3-8eef-eb2c4526f1da')
+ @decorators.idempotent_id('d873740a-7b10-40a9-943d-7cc18115370e')
+ @test.requires_ext(extension='OS-EXT-AZ', service='compute')
@rbac_rule_validation.action(
service="nova",
- rule="os_compute_api:os-instance-actions")
- def test_list_instance_actions(self):
- """Test list instance actions, part of os-instance-actions."""
- self.rbac_utils.switch_role(self, toggle_rbac_role=True)
- self.servers_client.list_instance_actions(self.server['id'])
+ rule="os_compute_api:os-extended-availability-zone")
+ def test_list_servers_with_details_extended_availability_zone(self):
+ """Test list servers OS-EXT-AZ:availability_zone attr in resp body."""
+ expected_attr = 'OS-EXT-AZ:availability_zone'
- @test.requires_ext(extension='os-instance-actions', service='compute')
- @decorators.idempotent_id('eb04c439-4215-4029-9ccb-5b3c041bfc25')
+ self.rbac_utils.switch_role(self, toggle_rbac_role=True)
+ 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(
+ attribute=expected_attr)
+
+ @decorators.idempotent_id('727e5360-770a-4b9c-8015-513a40216635')
+ @test.requires_ext(extension='OS-EXT-AZ', service='compute')
@rbac_rule_validation.action(
service="nova",
- rule="os_compute_api:os-instance-actions:events")
- def test_show_instance_action(self):
- """Test show instance action, part of os-instance-actions.
-
- Expect "events" details to be included in the response body.
- """
- # NOTE: "os_compute_api:os-instance-actions" is also enforced.
- request_id = self.server.response['x-compute-request-id']
+ rule="os_compute_api:os-extended-availability-zone")
+ def test_show_server_extended_availability_zone(self):
+ """Test show server OS-EXT-AZ:availability_zone attr in resp body."""
+ expected_attr = 'OS-EXT-AZ:availability_zone'
self.rbac_utils.switch_role(self, toggle_rbac_role=True)
- instance_action = self.servers_client.show_instance_action(
- self.server['id'], request_id)['instanceAction']
-
- if 'events' not in instance_action:
+ body = self.servers_client.show_server(self.server['id'])['server']
+ if expected_attr not in body:
raise rbac_exceptions.RbacMalformedResponse(
- 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(
- attribute='events.traceback')
+ attribute=expected_attr)
@decorators.idempotent_id('82053c27-3134-4003-9b55-bc9fafdb0e3b')
@test.requires_ext(extension='OS-EXT-STS', service='compute')
@@ -237,30 +234,33 @@
raise rbac_exceptions.RbacMalformedResponse(
attribute=attr)
- @decorators.idempotent_id('d873740a-7b10-40a9-943d-7cc18115370e')
- @test.requires_ext(extension='OS-EXT-AZ', service='compute')
+ @decorators.idempotent_id('21e39cbe-6c32-48fc-80dd-3e1fece6053f')
+ @test.requires_ext(extension='os-extended-volumes', service='compute')
@rbac_rule_validation.action(
service="nova",
- rule="os_compute_api:os-extended-availability-zone")
- def test_list_servers_with_details_extended_availability_zone(self):
- """Test list servers OS-EXT-AZ:availability_zone attr in resp body."""
- expected_attr = 'OS-EXT-AZ:availability_zone'
+ rule="os_compute_api:os-extended-volumes")
+ def test_list_servers_with_details_extended_volumes(self):
+ """Test list servers os-extended-volumes:volumes_attached attr in resp
+ body.
+ """
+ expected_attr = 'os-extended-volumes:volumes_attached'
self.rbac_utils.switch_role(self, toggle_rbac_role=True)
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(
attribute=expected_attr)
- @decorators.idempotent_id('727e5360-770a-4b9c-8015-513a40216635')
- @test.requires_ext(extension='OS-EXT-AZ', service='compute')
+ @decorators.idempotent_id('7f163708-0d25-4138-8512-dfdd72a92989')
+ @test.requires_ext(extension='os-extended-volumes', service='compute')
@rbac_rule_validation.action(
service="nova",
- rule="os_compute_api:os-extended-availability-zone")
- def test_show_server_extended_availability_zone(self):
- """Test show server OS-EXT-AZ:availability_zone attr in resp body."""
- expected_attr = 'OS-EXT-AZ:availability_zone'
+ rule="os_compute_api:os-extended-volumes")
+ def test_show_server_extended_volumes(self):
+ """Test show server os-extended-volumes:volumes_attached attr in resp
+ body.
+ """
+ expected_attr = 'os-extended-volumes:volumes_attached'
self.rbac_utils.switch_role(self, toggle_rbac_role=True)
body = self.servers_client.show_server(self.server['id'])['server']
@@ -268,6 +268,42 @@
raise rbac_exceptions.RbacMalformedResponse(
attribute=expected_attr)
+ @test.requires_ext(extension='os-instance-actions', service='compute')
+ @decorators.idempotent_id('9d1b131d-407e-4fa3-8eef-eb2c4526f1da')
+ @rbac_rule_validation.action(
+ service="nova",
+ rule="os_compute_api:os-instance-actions")
+ def test_list_instance_actions(self):
+ """Test list instance actions, part of os-instance-actions."""
+ self.rbac_utils.switch_role(self, toggle_rbac_role=True)
+ self.servers_client.list_instance_actions(self.server['id'])
+
+ @test.requires_ext(extension='os-instance-actions', service='compute')
+ @decorators.idempotent_id('eb04c439-4215-4029-9ccb-5b3c041bfc25')
+ @rbac_rule_validation.action(
+ service="nova",
+ rule="os_compute_api:os-instance-actions:events")
+ def test_show_instance_action(self):
+ """Test show instance action, part of os-instance-actions.
+
+ Expect "events" details to be included in the response body.
+ """
+ # NOTE: "os_compute_api:os-instance-actions" is also enforced.
+ request_id = self.server.response['x-compute-request-id']
+
+ self.rbac_utils.switch_role(self, toggle_rbac_role=True)
+ instance_action = self.servers_client.show_instance_action(
+ self.server['id'], request_id)['instanceAction']
+
+ if 'events' not in instance_action:
+ raise rbac_exceptions.RbacMalformedResponse(
+ 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(
+ attribute='events.traceback')
+
@rbac_rule_validation.action(
service="nova",
rule="os_compute_api:os-lock-server:lock")
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 9ed9eb6..45a5cda 100644
--- a/patrole_tempest_plugin/tests/api/network/test_routers_rbac.py
+++ b/patrole_tempest_plugin/tests/api/network/test_routers_rbac.py
@@ -296,8 +296,7 @@
distributed=False)
@rbac_rule_validation.action(service="neutron",
- rule="delete_router",
- expected_error_code=404)
+ rule="delete_router")
@decorators.idempotent_id('c0634dd5-0467-48f7-a4ae-1014d8edb2a7')
def test_delete_router(self):
"""Delete Router
@@ -309,10 +308,9 @@
self.routers_client.delete_router(router['id'])
@rbac_rule_validation.action(service="neutron",
- rule="add_router_interface",
- expected_error_code=404)
+ rule="add_router_interface")
@decorators.idempotent_id('a0627778-d68d-4913-881b-e345360cca19')
- def test_add_router_interfaces(self):
+ def test_add_router_interface(self):
"""Add Router Interface
RBAC test for the neutron add_router_interface policy
@@ -331,10 +329,9 @@
subnet_id=subnet['id'])
@rbac_rule_validation.action(service="neutron",
- rule="remove_router_interface",
- expected_error_code=404)
+ rule="remove_router_interface")
@decorators.idempotent_id('ff2593a4-2bff-4c27-97d3-dd3702b27dfb')
- def test_remove_router_interfaces(self):
+ def test_remove_router_interface(self):
"""Remove Router Interface
RBAC test for the neutron remove_router_interface policy
diff --git a/patrole_tempest_plugin/tests/unit/fixtures.py b/patrole_tempest_plugin/tests/unit/fixtures.py
index 9d53eb9..ce13029 100644
--- a/patrole_tempest_plugin/tests/unit/fixtures.py
+++ b/patrole_tempest_plugin/tests/unit/fixtures.py
@@ -73,7 +73,7 @@
'os_primary.credentials.project_id': self.PROJECT_ID,
'get_identity_version.return_value': 'v3'
}
- self.mock_test_obj = mock.Mock(**test_obj_kwargs)
+ self.mock_test_obj = mock.Mock(__name__='foo', **test_obj_kwargs)
# Mock out functionality that can't be used by unit tests.
self.mock_time = mock.patch.object(rbac_utils, 'time').start()
diff --git a/patrole_tempest_plugin/tests/unit/test_rbac_policy_parser.py b/patrole_tempest_plugin/tests/unit/test_policy_authority.py
similarity index 74%
rename from patrole_tempest_plugin/tests/unit/test_rbac_policy_parser.py
rename to patrole_tempest_plugin/tests/unit/test_policy_authority.py
index 6f173a2..2a8da9d 100644
--- a/patrole_tempest_plugin/tests/unit/test_rbac_policy_parser.py
+++ b/patrole_tempest_plugin/tests/unit/test_policy_authority.py
@@ -20,13 +20,14 @@
from tempest import config
from tempest.tests import base
+from patrole_tempest_plugin import policy_authority
from patrole_tempest_plugin import rbac_exceptions
-from patrole_tempest_plugin import rbac_policy_parser
+from patrole_tempest_plugin.tests.unit import fixtures
CONF = config.CONF
-class RbacPolicyTest(base.TestCase):
+class PolicyAuthorityTest(base.TestCase):
services = {
'services': [
@@ -39,9 +40,9 @@
}
def setUp(self):
- super(RbacPolicyTest, self).setUp()
- self.patchobject(rbac_policy_parser, 'credentials')
- m_creds = self.patchobject(rbac_policy_parser, 'clients')
+ super(PolicyAuthorityTest, self).setUp()
+ self.patchobject(policy_authority, 'credentials')
+ m_creds = self.patchobject(policy_authority, 'clients')
m_creds.Manager().identity_services_client.list_services.\
return_value = self.services
m_creds.Manager().identity_services_v3_client.list_services.\
@@ -63,36 +64,29 @@
self.conf_policy_path = os.path.join(
current_directory, 'resources', '%s.json')
- CONF.set_override(
- 'custom_policy_files', [self.conf_policy_path], group='patrole')
- self.addCleanup(CONF.clear_override, 'custom_policy_files',
- group='patrole')
+ self.useFixture(fixtures.ConfPatcher(
+ custom_policy_files=[self.conf_policy_path], group='patrole'))
+ self.useFixture(fixtures.ConfPatcher(
+ api_v3=True, api_v2=False, group='identity-feature-enabled'))
# Guarantee a blank slate for each test.
for attr in ('available_services', 'policy_files'):
- if attr in dir(rbac_policy_parser.RbacPolicyParser):
- delattr(rbac_policy_parser.RbacPolicyParser, attr)
-
- # TODO(fm577c): Use fixture for setting/clearing CONF.
- CONF.set_override('api_v3', True, group='identity-feature-enabled')
- self.addCleanup(CONF.clear_override, 'api_v2',
- group='identity-feature-enabled')
- self.addCleanup(CONF.clear_override, 'api_v3',
- group='identity-feature-enabled')
+ if attr in dir(policy_authority.PolicyAuthority):
+ delattr(policy_authority.PolicyAuthority, attr)
def _get_fake_policy_rule(self, name, rule):
- fake_rule = mock.Mock(check=rule)
+ fake_rule = mock.Mock(check=rule, __name__='foo')
fake_rule.name = name
return fake_rule
- @mock.patch.object(rbac_policy_parser, 'LOG', autospec=True)
+ @mock.patch.object(policy_authority, 'LOG', autospec=True)
def test_custom_policy(self, m_log):
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
- parser = rbac_policy_parser.RbacPolicyParser(
+ authority = policy_authority.PolicyAuthority(
test_tenant_id, test_user_id, "custom_rbac_policy")
expected = {
@@ -107,14 +101,14 @@
for rule, role_list in expected.items():
for role in role_list:
- self.assertTrue(parser.allowed(rule, role))
+ self.assertTrue(authority.allowed(rule, role))
for role in set(default_roles) - set(role_list):
- self.assertFalse(parser.allowed(rule, role))
+ self.assertFalse(authority.allowed(rule, role))
def test_admin_policy_file_with_admin_role(self):
test_tenant_id = mock.sentinel.tenant_id
test_user_id = mock.sentinel.user_id
- parser = rbac_policy_parser.RbacPolicyParser(
+ authority = policy_authority.PolicyAuthority(
test_tenant_id, test_user_id, "admin_rbac_policy")
role = 'admin'
@@ -124,17 +118,17 @@
disallowed_rules = ['non_admin_rule']
for rule in allowed_rules:
- allowed = parser.allowed(rule, role)
+ allowed = authority.allowed(rule, role)
self.assertTrue(allowed)
for rule in disallowed_rules:
- allowed = parser.allowed(rule, role)
+ allowed = authority.allowed(rule, role)
self.assertFalse(allowed)
def test_admin_policy_file_with_member_role(self):
test_tenant_id = mock.sentinel.tenant_id
test_user_id = mock.sentinel.user_id
- parser = rbac_policy_parser.RbacPolicyParser(
+ authority = policy_authority.PolicyAuthority(
test_tenant_id, test_user_id, "admin_rbac_policy")
role = 'Member'
@@ -145,17 +139,17 @@
'admin_rule', 'is_admin_rule', 'alt_admin_rule']
for rule in allowed_rules:
- allowed = parser.allowed(rule, role)
+ allowed = authority.allowed(rule, role)
self.assertTrue(allowed)
for rule in disallowed_rules:
- allowed = parser.allowed(rule, role)
+ allowed = authority.allowed(rule, role)
self.assertFalse(allowed)
def test_alt_admin_policy_file_with_context_is_admin(self):
test_tenant_id = mock.sentinel.tenant_id
test_user_id = mock.sentinel.user_id
- parser = rbac_policy_parser.RbacPolicyParser(
+ authority = policy_authority.PolicyAuthority(
test_tenant_id, test_user_id, "alt_admin_rbac_policy")
role = 'fake_admin'
@@ -163,11 +157,11 @@
disallowed_rules = ['admin_rule']
for rule in allowed_rules:
- allowed = parser.allowed(rule, role)
+ allowed = authority.allowed(rule, role)
self.assertTrue(allowed)
for rule in disallowed_rules:
- allowed = parser.allowed(rule, role)
+ allowed = authority.allowed(rule, role)
self.assertFalse(allowed)
role = 'super_admin'
@@ -175,11 +169,11 @@
disallowed_rules = ['non_admin_rule']
for rule in allowed_rules:
- allowed = parser.allowed(rule, role)
+ allowed = authority.allowed(rule, role)
self.assertTrue(allowed)
for rule in disallowed_rules:
- allowed = parser.allowed(rule, role)
+ allowed = authority.allowed(rule, role)
self.assertFalse(allowed)
def test_tenant_user_policy(self):
@@ -191,28 +185,28 @@
"""
test_tenant_id = mock.sentinel.tenant_id
test_user_id = mock.sentinel.user_id
- parser = rbac_policy_parser.RbacPolicyParser(
+ authority = policy_authority.PolicyAuthority(
test_tenant_id, test_user_id, "tenant_rbac_policy")
# Check whether Member role can perform expected actions.
allowed_rules = ['rule1', 'rule2', 'rule3', 'rule4']
for rule in allowed_rules:
- allowed = parser.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(parser.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 = parser.allowed(rule, 'admin')
+ allowed = authority.allowed(rule, 'admin')
self.assertTrue(allowed)
# Check whether _try_rule is called with the correct target dictionary.
with mock.patch.object(
- parser, '_try_rule', return_value=True, autospec=True) \
+ authority, '_try_rule', return_value=True, autospec=True) \
as mock_try_rule:
expected_target = {
@@ -232,20 +226,20 @@
}
for rule in allowed_rules:
- allowed = parser.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)
mock_try_rule.reset_mock()
- @mock.patch.object(rbac_policy_parser, 'LOG', autospec=True)
+ @mock.patch.object(policy_authority, 'LOG', autospec=True)
def test_invalid_service_raises_exception(self, m_log):
test_tenant_id = mock.sentinel.tenant_id
test_user_id = mock.sentinel.user_id
service = 'invalid_service'
self.assertRaises(rbac_exceptions.RbacInvalidService,
- rbac_policy_parser.RbacPolicyParser,
+ policy_authority.PolicyAuthority,
test_tenant_id,
test_user_id,
service)
@@ -253,25 +247,25 @@
m_log.debug.assert_called_once_with(
'%s is NOT a valid service.', service)
- @mock.patch.object(rbac_policy_parser, 'LOG', autospec=True)
+ @mock.patch.object(policy_authority, 'LOG', autospec=True)
def test_service_is_none_raises_exception(self, m_log):
test_tenant_id = mock.sentinel.tenant_id
test_user_id = mock.sentinel.user_id
service = None
self.assertRaises(rbac_exceptions.RbacInvalidService,
- rbac_policy_parser.RbacPolicyParser,
+ policy_authority.PolicyAuthority,
test_tenant_id,
test_user_id,
service)
m_log.debug.assert_called_once_with('%s is NOT a valid service.', None)
- @mock.patch.object(rbac_policy_parser, 'LOG', autospec=True)
+ @mock.patch.object(policy_authority, 'LOG', autospec=True)
def test_invalid_policy_rule_throws_rbac_parsing_exception(self, m_log):
test_tenant_id = mock.sentinel.tenant_id
test_user_id = mock.sentinel.user_id
- parser = rbac_policy_parser.RbacPolicyParser(
+ authority = policy_authority.PolicyAuthority(
test_tenant_id, test_user_id, "custom_rbac_policy")
fake_rule = 'fake_rule'
@@ -279,18 +273,19 @@
.format(fake_rule, self.custom_policy_file)
e = self.assertRaises(rbac_exceptions.RbacParsingException,
- parser.allowed, fake_rule, None)
+ authority.allowed, fake_rule, None)
self.assertIn(expected_message, str(e))
m_log.debug.assert_called_once_with(expected_message)
- @mock.patch.object(rbac_policy_parser, 'LOG', autospec=True)
+ @mock.patch.object(policy_authority, 'LOG', autospec=True)
def test_unknown_exception_throws_rbac_parsing_exception(self, m_log):
test_tenant_id = mock.sentinel.tenant_id
test_user_id = mock.sentinel.user_id
- parser = rbac_policy_parser.RbacPolicyParser(
+ authority = policy_authority.PolicyAuthority(
test_tenant_id, test_user_id, "custom_rbac_policy")
- parser.rules = mock.MagicMock(
+ authority.rules = mock.MagicMock(
+ __name__='foo',
**{'__getitem__.return_value.side_effect': Exception(
mock.sentinel.error)})
@@ -299,11 +294,11 @@
self.custom_policy_file)
e = self.assertRaises(rbac_exceptions.RbacParsingException,
- parser.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)
- @mock.patch.object(rbac_policy_parser, 'stevedore', autospec=True)
+ @mock.patch.object(policy_authority, 'stevedore', autospec=True)
def test_get_policy_data_from_file_and_from_code(self, mock_stevedore):
fake_policy_rules = [
self._get_fake_policy_rule('code_policy_action_1',
@@ -314,7 +309,7 @@
'rule:code_rule_3'),
]
- mock_manager = mock.Mock(obj=fake_policy_rules)
+ mock_manager = mock.Mock(obj=fake_policy_rules, __name__='foo')
mock_manager.configure_mock(name='fake_service')
mock_stevedore.named.NamedExtensionManager.return_value = [
mock_manager
@@ -322,10 +317,10 @@
test_tenant_id = mock.sentinel.tenant_id
test_user_id = mock.sentinel.user_id
- parser = rbac_policy_parser.RbacPolicyParser(
+ authority = policy_authority.PolicyAuthority(
test_tenant_id, test_user_id, "tenant_rbac_policy")
- policy_data = parser._get_policy_data('fake_service')
+ policy_data = authority._get_policy_data('fake_service')
self.assertIsInstance(policy_data, str)
actual_policy_data = json.loads(policy_data)
@@ -343,7 +338,7 @@
self.assertEqual(expected_policy_data, actual_policy_data)
- @mock.patch.object(rbac_policy_parser, 'stevedore', autospec=True)
+ @mock.patch.object(policy_authority, 'stevedore', autospec=True)
def test_get_policy_data_from_file_and_from_code_with_overwrite(
self, mock_stevedore):
# The custom policy file should overwrite default rules rule1 and rule2
@@ -355,7 +350,7 @@
'rule:code_rule_3'),
]
- mock_manager = mock.Mock(obj=fake_policy_rules)
+ mock_manager = mock.Mock(obj=fake_policy_rules, __name__='foo')
mock_manager.configure_mock(name='fake_service')
mock_stevedore.named.NamedExtensionManager.return_value = [
mock_manager
@@ -364,9 +359,9 @@
test_tenant_id = mock.sentinel.tenant_id
test_user_id = mock.sentinel.user_id
- parser = rbac_policy_parser.RbacPolicyParser(
+ authority = policy_authority.PolicyAuthority(
test_tenant_id, test_user_id, 'tenant_rbac_policy')
- policy_data = parser._get_policy_data('fake_service')
+ policy_data = authority._get_policy_data('fake_service')
self.assertIsInstance(policy_data, str)
actual_policy_data = json.loads(policy_data)
@@ -382,11 +377,11 @@
self.assertEqual(expected_policy_data, actual_policy_data)
- @mock.patch.object(rbac_policy_parser, 'stevedore', autospec=True)
+ @mock.patch.object(policy_authority, 'stevedore', autospec=True)
def test_get_policy_data_cannot_find_policy(self, mock_stevedore):
mock_stevedore.named.NamedExtensionManager.return_value = None
e = self.assertRaises(rbac_exceptions.RbacParsingException,
- rbac_policy_parser.RbacPolicyParser,
+ policy_authority.PolicyAuthority,
None, None, 'test_service')
expected_error = \
@@ -397,14 +392,14 @@
self.assertIn(expected_error, str(e))
- @mock.patch.object(rbac_policy_parser, 'json', autospec=True)
- @mock.patch.object(rbac_policy_parser, 'stevedore', autospec=True)
+ @mock.patch.object(policy_authority, 'json', autospec=True)
+ @mock.patch.object(policy_authority, 'stevedore', autospec=True)
def test_get_policy_data_without_valid_policy(self, mock_stevedore,
mock_json):
- test_policy_action = mock.Mock(check='rule:bar')
+ test_policy_action = mock.Mock(check='rule:bar', __name__='foo')
test_policy_action.configure_mock(name='foo')
- test_policy = mock.Mock(obj=[test_policy_action])
+ test_policy = mock.Mock(obj=[test_policy_action], __name__='foo')
test_policy.configure_mock(name='test_service')
mock_stevedore.named.NamedExtensionManager\
@@ -413,7 +408,7 @@
mock_json.dumps.side_effect = ValueError
e = self.assertRaises(rbac_exceptions.RbacParsingException,
- rbac_policy_parser.RbacPolicyParser,
+ policy_authority.PolicyAuthority,
None, None, 'test_service')
expected_error = "Policy file for {0} service is invalid."\
@@ -427,14 +422,14 @@
invoke_on_load=True,
warn_on_missing_entrypoint=False)
- @mock.patch.object(rbac_policy_parser, 'json', autospec=True)
- @mock.patch.object(rbac_policy_parser, 'stevedore', autospec=True)
+ @mock.patch.object(policy_authority, 'json', autospec=True)
+ @mock.patch.object(policy_authority, 'stevedore', autospec=True)
def test_get_policy_data_from_file_not_json(self, mock_stevedore,
mock_json):
mock_stevedore.named.NamedExtensionManager.return_value = None
mock_json.loads.side_effect = ValueError
e = self.assertRaises(rbac_exceptions.RbacParsingException,
- rbac_policy_parser.RbacPolicyParser,
+ policy_authority.PolicyAuthority,
None, None, 'tenant_rbac_policy')
expected_error = (
@@ -444,22 +439,22 @@
self.assertIn(expected_error, str(e))
def test_discover_policy_files(self):
- policy_parser = rbac_policy_parser.RbacPolicyParser(
+ policy_parser = policy_authority.PolicyAuthority(
None, None, 'tenant_rbac_policy')
# Ensure that "policy_files" is set at class and instance levels.
self.assertIn('policy_files',
- dir(rbac_policy_parser.RbacPolicyParser))
+ dir(policy_authority.PolicyAuthority))
self.assertIn('policy_files', dir(policy_parser))
self.assertIn('tenant_rbac_policy', policy_parser.policy_files)
self.assertEqual(self.conf_policy_path % 'tenant_rbac_policy',
policy_parser.policy_files['tenant_rbac_policy'])
- @mock.patch.object(rbac_policy_parser, 'policy', autospec=True)
- @mock.patch.object(rbac_policy_parser.RbacPolicyParser, '_get_policy_data',
+ @mock.patch.object(policy_authority, 'policy', autospec=True)
+ @mock.patch.object(policy_authority.PolicyAuthority, '_get_policy_data',
autospec=True)
- @mock.patch.object(rbac_policy_parser, 'clients', autospec=True)
- @mock.patch.object(rbac_policy_parser, 'os', autospec=True)
+ @mock.patch.object(policy_authority, 'clients', autospec=True)
+ @mock.patch.object(policy_authority, 'os', autospec=True)
def test_discover_policy_files_with_many_invalid_one_valid(self, m_os,
m_creds, *args):
# Only the 3rd path is valid.
@@ -471,16 +466,16 @@
'services': [{'name': 'test_service'}]}
# The expected policy will be 'baz/test_service'.
- CONF.set_override(
- 'custom_policy_files', ['foo/%s', 'bar/%s', 'baz/%s'],
- group='patrole')
+ self.useFixture(fixtures.ConfPatcher(
+ custom_policy_files=['foo/%s', 'bar/%s', 'baz/%s'],
+ group='patrole'))
- policy_parser = rbac_policy_parser.RbacPolicyParser(
+ policy_parser = policy_authority.PolicyAuthority(
None, None, 'test_service')
# Ensure that "policy_files" is set at class and instance levels.
self.assertIn('policy_files',
- dir(rbac_policy_parser.RbacPolicyParser))
+ dir(policy_authority.PolicyAuthority))
self.assertIn('policy_files', dir(policy_parser))
self.assertIn('test_service', policy_parser.policy_files)
self.assertEqual('baz/test_service',
@@ -492,19 +487,19 @@
[self.conf_policy_path % 'test_service'])
e = self.assertRaises(rbac_exceptions.RbacParsingException,
- rbac_policy_parser.RbacPolicyParser,
+ policy_authority.PolicyAuthority,
None, None, 'test_service')
self.assertIn(expected_error, str(e))
self.assertIn('policy_files',
- dir(rbac_policy_parser.RbacPolicyParser))
+ dir(policy_authority.PolicyAuthority))
self.assertNotIn(
'test_service',
- rbac_policy_parser.RbacPolicyParser.policy_files.keys())
+ policy_authority.PolicyAuthority.policy_files.keys())
def _test_validate_service(self, v2_services, v3_services,
expected_failure=False, expected_services=None):
- with mock.patch.object(rbac_policy_parser, 'clients') as m_creds:
+ with mock.patch.object(policy_authority, 'clients') as m_creds:
m_creds.Manager().identity_services_client.list_services.\
return_value = v2_services
m_creds.Manager().identity_services_v3_client.list_services.\
@@ -513,15 +508,15 @@
test_tenant_id = mock.sentinel.tenant_id
test_user_id = mock.sentinel.user_id
- mock_os = self.patchobject(rbac_policy_parser, 'os')
+ mock_os = self.patchobject(policy_authority, 'os')
mock_os.path.join.return_value = self.admin_policy_file
if not expected_services:
expected_services = [s['name'] for s in self.services['services']]
# Guarantee a blank slate for this test.
- if hasattr(rbac_policy_parser.RbacPolicyParser, 'available_services'):
- delattr(rbac_policy_parser.RbacPolicyParser,
+ if hasattr(policy_authority.PolicyAuthority, 'available_services'):
+ delattr(policy_authority.PolicyAuthority,
'available_services')
if expected_failure:
@@ -530,10 +525,10 @@
expected_exception = 'invalid_service is NOT a valid service'
with self.assertRaisesRegex(rbac_exceptions.RbacInvalidService,
expected_exception):
- rbac_policy_parser.RbacPolicyParser(
+ policy_authority.PolicyAuthority(
test_tenant_id, test_user_id, "INVALID_SERVICE")
else:
- policy_parser = rbac_policy_parser.RbacPolicyParser(
+ policy_parser = policy_authority.PolicyAuthority(
test_tenant_id, test_user_id, "tenant_rbac_policy")
# Check that the attribute is available at object and class levels.
@@ -542,11 +537,11 @@
self.assertTrue(hasattr(policy_parser, 'available_services'))
self.assertEqual(expected_services,
policy_parser.available_services)
- self.assertTrue(hasattr(rbac_policy_parser.RbacPolicyParser,
+ self.assertTrue(hasattr(policy_authority.PolicyAuthority,
'available_services'))
self.assertEqual(
expected_services,
- rbac_policy_parser.RbacPolicyParser.available_services)
+ policy_authority.PolicyAuthority.available_services)
def test_validate_service(self):
"""Positive test case to ensure ``validate_service`` works.
@@ -556,16 +551,16 @@
2) Identity v2 API enabled.
3) Both are enabled.
"""
- CONF.set_override('api_v2', True, group='identity-feature-enabled')
- CONF.set_override('api_v3', False, group='identity-feature-enabled')
+ self.useFixture(fixtures.ConfPatcher(
+ api_v2=True, api_v3=False, group='identity-feature-enabled'))
self._test_validate_service(self.services, [], False)
- CONF.set_override('api_v2', False, group='identity-feature-enabled')
- CONF.set_override('api_v3', True, group='identity-feature-enabled')
+ self.useFixture(fixtures.ConfPatcher(
+ api_v2=False, api_v3=True, group='identity-feature-enabled'))
self._test_validate_service([], self.services, False)
- CONF.set_override('api_v2', True, group='identity-feature-enabled')
- CONF.set_override('api_v3', True, group='identity-feature-enabled')
+ self.useFixture(fixtures.ConfPatcher(
+ api_v2=True, api_v3=True, group='identity-feature-enabled'))
self._test_validate_service(self.services, self.services, False)
def test_validate_service_except_invalid_service(self):
@@ -577,18 +572,18 @@
3) Both are enabled.
4) Neither are enabled.
"""
- CONF.set_override('api_v2', True, group='identity-feature-enabled')
- CONF.set_override('api_v3', False, group='identity-feature-enabled')
+ self.useFixture(fixtures.ConfPatcher(
+ api_v2=True, api_v3=False, group='identity-feature-enabled'))
self._test_validate_service(self.services, [], True)
- CONF.set_override('api_v2', False, group='identity-feature-enabled')
- CONF.set_override('api_v3', True, group='identity-feature-enabled')
+ self.useFixture(fixtures.ConfPatcher(
+ api_v2=False, api_v3=True, group='identity-feature-enabled'))
self._test_validate_service([], self.services, True)
- CONF.set_override('api_v2', True, group='identity-feature-enabled')
- CONF.set_override('api_v3', True, group='identity-feature-enabled')
+ self.useFixture(fixtures.ConfPatcher(
+ api_v2=True, api_v3=True, group='identity-feature-enabled'))
self._test_validate_service(self.services, self.services, True)
- CONF.set_override('api_v2', False, group='identity-feature-enabled')
- CONF.set_override('api_v3', False, group='identity-feature-enabled')
+ self.useFixture(fixtures.ConfPatcher(
+ api_v2=False, api_v3=False, group='identity-feature-enabled'))
self._test_validate_service([], [], 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 8a69ff6..94a2306 100644
--- a/patrole_tempest_plugin/tests/unit/test_rbac_rule_validation.py
+++ b/patrole_tempest_plugin/tests/unit/test_rbac_rule_validation.py
@@ -40,9 +40,12 @@
CONF.set_override('rbac_test_role', 'Member', group='patrole')
self.addCleanup(CONF.clear_override, 'rbac_test_role', group='patrole')
+ self.mock_rbaclog = mock.patch.object(
+ rbac_rv.RBACLOG, 'info', autospec=False).start()
+
@mock.patch.object(rbac_rv, 'LOG', autospec=True)
- @mock.patch.object(rbac_rv, 'rbac_policy_parser', autospec=True)
- def test_rule_validation_have_permission_no_exc(self, mock_policy,
+ @mock.patch.object(rbac_rv, 'policy_authority', autospec=True)
+ def test_rule_validation_have_permission_no_exc(self, mock_authority,
mock_log):
"""Test that having permission and no exception thrown is success.
@@ -50,10 +53,11 @@
"""
decorator = rbac_rv.action(mock.sentinel.service, mock.sentinel.action)
- mock_function = mock.Mock()
+ mock_function = mock.Mock(__name__='foo')
wrapper = decorator(mock_function)
- mock_policy.RbacPolicyParser.return_value.allowed.return_value = True
+ mock_authority.PolicyAuthority.return_value.allowed\
+ .return_value = True
result = wrapper(self.mock_args)
@@ -62,8 +66,8 @@
mock_log.error.assert_not_called()
@mock.patch.object(rbac_rv, 'LOG', autospec=True)
- @mock.patch.object(rbac_rv, 'rbac_policy_parser', autospec=True)
- def test_rule_validation_lack_permission_throw_exc(self, mock_policy,
+ @mock.patch.object(rbac_rv, 'policy_authority', autospec=True)
+ def test_rule_validation_lack_permission_throw_exc(self, mock_authority,
mock_log):
"""Test that having no permission and exception thrown is success.
@@ -71,11 +75,12 @@
"""
decorator = rbac_rv.action(mock.sentinel.service, mock.sentinel.action)
- mock_function = mock.Mock()
+ mock_function = mock.Mock(__name__='foo')
mock_function.side_effect = exceptions.Forbidden
wrapper = decorator(mock_function)
- mock_policy.RbacPolicyParser.return_value.allowed.return_value = False
+ mock_authority.PolicyAuthority.return_value.allowed\
+ .return_value = False
result = wrapper(self.mock_args)
@@ -84,8 +89,9 @@
mock_log.error.assert_not_called()
@mock.patch.object(rbac_rv, 'LOG', autospec=True)
- @mock.patch.object(rbac_rv, 'rbac_policy_parser', autospec=True)
- def test_rule_validation_forbidden_negative(self, mock_policy, mock_log):
+ @mock.patch.object(rbac_rv, 'policy_authority', autospec=True)
+ def test_rule_validation_forbidden_negative(self, mock_authority,
+ mock_log):
"""Test Forbidden error is thrown and have permission fails.
Negative test case: if Forbidden is thrown and the user should be
@@ -93,11 +99,12 @@
raised.
"""
decorator = rbac_rv.action(mock.sentinel.service, mock.sentinel.action)
- mock_function = mock.Mock()
+ mock_function = mock.Mock(__name__='foo')
mock_function.side_effect = exceptions.Forbidden
wrapper = decorator(mock_function)
- mock_policy.RbacPolicyParser.return_value.allowed.return_value = True
+ mock_authority.PolicyAuthority.return_value.allowed\
+ .return_value = True
e = self.assertRaises(exceptions.Forbidden, wrapper, self.mock_args)
self.assertIn(
@@ -107,21 +114,21 @@
" perform sentinel.action.")
@mock.patch.object(rbac_rv, 'LOG', autospec=True)
- @mock.patch.object(rbac_rv, 'rbac_policy_parser', autospec=True)
- def test_rule_validation_rbac_malformed_response_positive(self,
- mock_policy,
- mock_log):
+ @mock.patch.object(rbac_rv, 'policy_authority', autospec=True)
+ def test_rule_validation_rbac_malformed_response_positive(
+ self, mock_authority_authority, mock_log):
"""Test RbacMalformedResponse error is thrown without permission passes.
Positive test case: if RbacMalformedResponse is thrown and the user is
not allowed to perform the action, then this is a success.
"""
decorator = rbac_rv.action(mock.sentinel.service, mock.sentinel.action)
- mock_function = mock.Mock()
+ mock_function = mock.Mock(__name__='foo')
mock_function.side_effect = rbac_exceptions.RbacMalformedResponse
wrapper = decorator(mock_function)
- mock_policy.RbacPolicyParser.return_value.allowed.return_value = False
+ (mock_authority_authority.PolicyAuthority.return_value.allowed
+ .return_value) = False
result = wrapper(self.mock_args)
@@ -130,21 +137,21 @@
mock_log.warning.assert_not_called()
@mock.patch.object(rbac_rv, 'LOG', autospec=True)
- @mock.patch.object(rbac_rv, 'rbac_policy_parser', autospec=True)
- def test_rule_validation_rbac_malformed_response_negative(self,
- mock_policy,
- mock_log):
+ @mock.patch.object(rbac_rv, 'policy_authority', autospec=True)
+ def test_rule_validation_rbac_malformed_response_negative(
+ self, mock_authority_authority, mock_log):
"""Test RbacMalformedResponse error is thrown with permission fails.
Negative test case: if RbacMalformedResponse is thrown and the user is
allowed to perform the action, then this is an expected failure.
"""
decorator = rbac_rv.action(mock.sentinel.service, mock.sentinel.action)
- mock_function = mock.Mock()
+ mock_function = mock.Mock(__name__='foo')
mock_function.side_effect = rbac_exceptions.RbacMalformedResponse
wrapper = decorator(mock_function)
- mock_policy.RbacPolicyParser.return_value.allowed.return_value = True
+ (mock_authority_authority.PolicyAuthority.return_value.allowed
+ .return_value) = True
e = self.assertRaises(exceptions.Forbidden, wrapper, self.mock_args)
self.assertIn(
@@ -155,21 +162,21 @@
" perform sentinel.action.")
@mock.patch.object(rbac_rv, 'LOG', autospec=True)
- @mock.patch.object(rbac_rv, 'rbac_policy_parser', autospec=True)
- def test_rule_validation_rbac_conflicting_policies_positive(self,
- mock_policy,
- mock_log):
+ @mock.patch.object(rbac_rv, 'policy_authority', autospec=True)
+ def test_rule_validation_rbac_conflicting_policies_positive(
+ self, mock_authority_authority, mock_log):
"""Test RbacConflictingPolicies error is thrown without permission passes.
Positive test case: if RbacConflictingPolicies is thrown and the user
is not allowed to perform the action, then this is a success.
"""
decorator = rbac_rv.action(mock.sentinel.service, mock.sentinel.action)
- mock_function = mock.Mock()
+ mock_function = mock.Mock(__name__='foo')
mock_function.side_effect = rbac_exceptions.RbacConflictingPolicies
wrapper = decorator(mock_function)
- mock_policy.RbacPolicyParser.return_value.allowed.return_value = False
+ (mock_authority_authority.PolicyAuthority.return_value.allowed
+ .return_value) = False
result = wrapper(self.mock_args)
@@ -178,9 +185,9 @@
mock_log.warning.assert_not_called()
@mock.patch.object(rbac_rv, 'LOG', autospec=True)
- @mock.patch.object(rbac_rv, 'rbac_policy_parser', autospec=True)
+ @mock.patch.object(rbac_rv, 'policy_authority', autospec=True)
def test_rule_validation_rbac_conflicting_policies_negative(self,
- mock_policy,
+ mock_authority,
mock_log):
"""Test RbacConflictingPolicies error is thrown with permission fails.
@@ -188,11 +195,12 @@
is allowed to perform the action, then this is an expected failure.
"""
decorator = rbac_rv.action(mock.sentinel.service, mock.sentinel.action)
- mock_function = mock.Mock()
+ mock_function = mock.Mock(__name__='foo')
mock_function.side_effect = rbac_exceptions.RbacConflictingPolicies
wrapper = decorator(mock_function)
- mock_policy.RbacPolicyParser.return_value.allowed.return_value = True
+ mock_authority.PolicyAuthority.return_value.allowed\
+ .return_value = True
e = self.assertRaises(exceptions.Forbidden, wrapper, self.mock_args)
self.assertIn(
@@ -203,8 +211,8 @@
" perform sentinel.action.")
@mock.patch.object(rbac_rv, 'LOG', autospec=True)
- @mock.patch.object(rbac_rv, 'rbac_policy_parser', autospec=True)
- def test_expect_not_found_but_raises_forbidden(self, mock_policy,
+ @mock.patch.object(rbac_rv, 'policy_authority', autospec=True)
+ def test_expect_not_found_but_raises_forbidden(self, mock_authority,
mock_log):
"""Test that expecting 404 but getting 403 works for all scenarios.
@@ -217,17 +225,16 @@
decorator = rbac_rv.action(mock.sentinel.service,
mock.sentinel.action,
expected_error_code=404)
- mock_function = mock.Mock()
+ mock_function = mock.Mock(__name__='foo')
mock_function.side_effect = exceptions.Forbidden('Random message.')
wrapper = decorator(mock_function)
- expected_error = "Forbidden\nDetails: Random message. An unexpected "\
- "exception has occurred: Expected exception was "\
- "NotFound, which was not thrown."
+ expected_error = "An unexpected exception has occurred during test: "\
+ "foo, Exception was: Forbidden\nDetails: Random message."
- for permission in [True, False]:
- mock_policy.RbacPolicyParser.return_value.allowed.return_value =\
- permission
+ for allowed in [True, False]:
+ mock_authority.PolicyAuthority.return_value.allowed.\
+ return_value = allowed
e = self.assertRaises(exceptions.Forbidden, wrapper,
self.mock_args)
@@ -236,8 +243,9 @@
mock_log.error.reset_mock()
@mock.patch.object(rbac_rv, 'LOG', autospec=True)
- @mock.patch.object(rbac_rv, 'rbac_policy_parser', autospec=True)
- def test_expect_not_found_and_raise_not_found(self, mock_policy, mock_log):
+ @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:
@@ -250,7 +258,7 @@
decorator = rbac_rv.action(mock.sentinel.service,
mock.sentinel.action,
expected_error_code=404)
- mock_function = mock.Mock()
+ mock_function = mock.Mock(__name__='foo')
mock_function.side_effect = exceptions.NotFound
wrapper = decorator(mock_function)
@@ -258,9 +266,9 @@
"Role Member was not allowed to perform sentinel.action.", None
]
- for pos, permission in enumerate([True, False]):
- mock_policy.RbacPolicyParser.return_value.allowed.return_value =\
- permission
+ for pos, allowed in enumerate([True, False]):
+ mock_authority.PolicyAuthority.return_value.allowed\
+ .return_value = allowed
expected_error = expected_errors[pos]
@@ -283,8 +291,8 @@
mock_log.error.reset_mock()
@mock.patch.object(rbac_rv, 'LOG', autospec=True)
- @mock.patch.object(rbac_rv, 'rbac_policy_parser', autospec=True)
- def test_rule_validation_overpermission_negative(self, mock_policy,
+ @mock.patch.object(rbac_rv, 'policy_authority', autospec=True)
+ def test_rule_validation_overpermission_negative(self, mock_authority,
mock_log):
"""Test that OverPermission is correctly handled.
@@ -293,45 +301,46 @@
"""
decorator = rbac_rv.action(mock.sentinel.service, mock.sentinel.action)
- mock_function = mock.Mock()
+ mock_function = mock.Mock(__name__='foo')
wrapper = decorator(mock_function)
- mock_policy.RbacPolicyParser.return_value.allowed.return_value = False
+ mock_authority.PolicyAuthority.return_value.allowed\
+ .return_value = False
e = self.assertRaises(rbac_exceptions.RbacOverPermission, wrapper,
self.mock_args)
self.assertIn(("OverPermission: Role Member was allowed to perform "
- "sentinel.action"), e.__str__())
+ "sentinel.action"), e.__str__())
mock_log.error.assert_called_once_with(
'Role %s was allowed to perform %s', 'Member',
mock.sentinel.action)
- @mock.patch.object(rbac_rv, 'rbac_policy_parser', autospec=True)
+ @mock.patch.object(rbac_rv, 'policy_authority', autospec=True)
def test_invalid_policy_rule_throws_parsing_exception(
- self, mock_rbac_policy_parser):
+ self, mock_authority_authority):
"""Test that invalid policy action causes test to be skipped."""
CONF.set_override('strict_policy_check', True, group='patrole')
self.addCleanup(CONF.clear_override, 'strict_policy_check',
group='patrole')
- mock_rbac_policy_parser.RbacPolicyParser.return_value.allowed.\
+ mock_authority_authority.PolicyAuthority.return_value.allowed.\
side_effect = rbac_exceptions.RbacParsingException
decorator = rbac_rv.action(mock.sentinel.service,
mock.sentinel.policy_rule)
- wrapper = decorator(mock.Mock())
+ wrapper = decorator(mock.Mock(__name__='foo'))
e = self.assertRaises(rbac_exceptions.RbacParsingException, wrapper,
self.mock_args)
self.assertEqual('Attempted to test an invalid policy file or action',
str(e))
- mock_rbac_policy_parser.RbacPolicyParser.assert_called_once_with(
+ mock_authority_authority.PolicyAuthority.assert_called_once_with(
mock.sentinel.project_id, mock.sentinel.user_id,
mock.sentinel.service, extra_target_data={})
- @mock.patch.object(rbac_rv, 'rbac_policy_parser', autospec=True)
- def test_get_exception_type_404(self, mock_policy):
+ @mock.patch.object(rbac_rv, 'policy_authority', autospec=True)
+ def test_get_exception_type_404(self, _):
"""Test that getting a 404 exception type returns NotFound."""
expected_exception = exceptions.NotFound
expected_irregular_msg = ("NotFound exception was caught for policy "
@@ -344,8 +353,8 @@
self.assertEqual(expected_exception, actual_exception)
self.assertEqual(expected_irregular_msg, actual_irregular_msg)
- @mock.patch.object(rbac_rv, 'rbac_policy_parser', autospec=True)
- def test_get_exception_type_403(self, mock_policy):
+ @mock.patch.object(rbac_rv, 'policy_authority', autospec=True)
+ def test_get_exception_type_403(self, _):
"""Test that getting a 404 exception type returns Forbidden."""
expected_exception = exceptions.Forbidden
expected_irregular_msg = None
@@ -356,10 +365,9 @@
self.assertEqual(expected_exception, actual_exception)
self.assertEqual(expected_irregular_msg, actual_irregular_msg)
+ @mock.patch.object(rbac_rv, 'policy_authority', autospec=True)
@mock.patch.object(rbac_rv, 'LOG', autospec=True)
- @mock.patch.object(rbac_rv, 'rbac_policy_parser', autospec=True)
- def test_exception_thrown_when_type_is_not_int(self, mock_policy,
- mock_log):
+ def test_exception_thrown_when_type_is_not_int(self, mock_log, _):
"""Test that non-integer exception type raises error."""
self.assertRaises(rbac_exceptions.RbacInvalidErrorCode,
rbac_rv._get_exception_type, "403")
@@ -368,10 +376,9 @@
"code. Currently supported "
"codes: [403, 404]")
+ @mock.patch.object(rbac_rv, 'policy_authority', autospec=True)
@mock.patch.object(rbac_rv, 'LOG', autospec=True)
- @mock.patch.object(rbac_rv, 'rbac_policy_parser', autospec=True)
- def test_exception_thrown_when_type_is_403_or_404(self, mock_policy,
- mock_log):
+ def test_exception_thrown_when_type_is_403_or_404(self, mock_log, _):
"""Test that unsupported exceptions throw error."""
invalid_exceptions = [200, 400, 500]
for exc in invalid_exceptions:
@@ -382,3 +389,51 @@
"codes: [403, 404]")
mock_log.error.reset_mock()
+
+ @mock.patch.object(rbac_rv, 'RBACLOG', autospec=True)
+ @mock.patch.object(rbac_rv, 'policy_authority', autospec=True)
+ def test_rbac_report_logging_disabled(self, mock_authority, mock_rbaclog):
+ """Test case to ensure that we DON'T write logs when
+ enable_reporting is False
+ """
+ CONF.set_override('enable_reporting', False, group='patrole_log')
+ self.addCleanup(CONF.clear_override,
+ 'enable_reporting', group='patrole_log')
+
+ decorator = rbac_rv.action(mock.sentinel.service, mock.sentinel.action)
+
+ mock_function = mock.Mock(__name__='foo-nolog')
+ wrapper = decorator(mock_function)
+
+ mock_authority.PolicyAuthority.return_value.allowed.return_value = True
+
+ wrapper(self.mock_args)
+
+ self.assertFalse(mock_rbaclog.info.called)
+
+ @mock.patch.object(rbac_rv, 'RBACLOG', autospec=True)
+ @mock.patch.object(rbac_rv, 'policy_authority', autospec=True)
+ def test_rbac_report_logging_enabled(self, mock_authority, mock_rbaclog):
+ """Test case to ensure that we DO write logs when
+ enable_reporting is True
+ """
+ CONF.set_override('enable_reporting', True, group='patrole_log')
+ self.addCleanup(CONF.clear_override,
+ 'enable_reporting', group='patrole_log')
+
+ decorator = rbac_rv.action(mock.sentinel.service, mock.sentinel.action)
+
+ mock_function = mock.Mock(__name__='foo-log')
+ wrapper = decorator(mock_function)
+
+ mock_authority.PolicyAuthority.return_value.allowed.return_value = True
+
+ wrapper(self.mock_args)
+
+ mock_rbaclog.info.assert_called_once_with(
+ "[Service]: %s, [Test]: %s, [Rule]: %s, "
+ "[Expected]: %s, [Actual]: %s",
+ mock.sentinel.service, 'foo-log',
+ mock.sentinel.action,
+ "Allowed",
+ "Allowed")
diff --git a/releasenotes/notes/flavor-manage-rbac-tests-eb78439316d67ab2.yaml b/releasenotes/notes/flavor-manage-rbac-tests-eb78439316d67ab2.yaml
new file mode 100644
index 0000000..0fbf24f
--- /dev/null
+++ b/releasenotes/notes/flavor-manage-rbac-tests-eb78439316d67ab2.yaml
@@ -0,0 +1,8 @@
+---
+features:
+ - |
+ Add test coverage for the os-flavor-manage compute API, which includes
+ tests for the following policy actions:
+
+ * "os_compute_api:os-flavor-manage:create"
+ * "os_compute_api:os-flavor-manage:delete"
diff --git a/releasenotes/notes/rbac-per-test-log-071a530e957c1c26.yaml b/releasenotes/notes/rbac-per-test-log-071a530e957c1c26.yaml
new file mode 100644
index 0000000..b1d400c
--- /dev/null
+++ b/releasenotes/notes/rbac-per-test-log-071a530e957c1c26.yaml
@@ -0,0 +1,28 @@
+---
+features:
+ - |
+ Added in a new logging feature which logs the result of each Patrole test
+
+ The format of the new log output is:
+
+ "[Service]: %s, [Test]: %s, [Rule]: %s, [Expected]: %s, [Actual]: %s"
+
+ where each "%s" is a string that contains:
+
+ * [Service] - The openstack service being tested (Nova, Neutron, etc)
+ * [Test] - The name of the test function being invoked (eg: test_list_aggregate_rbac)
+ * [Rule] - The name of the rule the Patrole test is testing (eg: os_compute_api:os-aggregates)
+ * [Expected] - The expected outcome (one of Allowed/Denied)
+ * [Actual] - The actual outcome from the Patrole test (one of Allowed/Denied/Error)
+
+ This logging feature has two config variables:
+
+ These variables are part of a new config group ``patrole_log``
+
+ * enable_reporting:
+ This enables or disables the enhanced rbac reporting
+ * report_log_name:
+ This variable specifies the name of the log file to write
+ * report_log_path:
+ This variable specifies the path (relative or absolute)
+ of the log file to write
diff --git a/releasenotes/notes/rbac-tests-for-compute-extended-volumes-7f3ccab122d22737.yaml b/releasenotes/notes/rbac-tests-for-compute-extended-volumes-7f3ccab122d22737.yaml
new file mode 100644
index 0000000..f7eb02d
--- /dev/null
+++ b/releasenotes/notes/rbac-tests-for-compute-extended-volumes-7f3ccab122d22737.yaml
@@ -0,0 +1,6 @@
+---
+features:
+ - |
+ Add RBAC tests for os-extended-volumes:volumes_attached policies, which
+ validate that "os-extended-volumes:volumes_attached" is returned in the
+ response body.
diff --git a/releasenotes/notes/start-of-pike-support-360e27b4d192e3d2.yaml b/releasenotes/notes/start-of-pike-support-360e27b4d192e3d2.yaml
new file mode 100644
index 0000000..50e9159
--- /dev/null
+++ b/releasenotes/notes/start-of-pike-support-360e27b4d192e3d2.yaml
@@ -0,0 +1,10 @@
+---
+prelude: >
+ This release marks the start of support for the Pike release in Patrole.
+other:
+ - OpenStack Releases supported after this release are **Pike**.
+
+ The release under current development of this tag is Queens, meaning
+ that every Patrole commit is also tested against master during the Queens
+ cycle. However, this does not necessarily mean that using Patrole as of
+ this tag will work against a Queens (or future release) cloud.
diff --git a/requirements.txt b/requirements.txt
index cd6a577..00c7e64 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -4,7 +4,7 @@
hacking!=0.13.0,<0.14,>=0.12.0 # Apache-2.0
pbr!=2.1.0,>=2.0.0 # Apache-2.0
urllib3>=1.21.1 # MIT
-oslo.log>=3.22.0 # Apache-2.0
+oslo.log>=3.30.0 # Apache-2.0
oslo.config!=4.3.0,!=4.4.0,>=4.0.0 # Apache-2.0
oslo.policy>=1.23.0 # Apache-2.0
tempest>=16.1.0 # Apache-2.0
diff --git a/test-requirements.txt b/test-requirements.txt
index 772694e..0657438 100644
--- a/test-requirements.txt
+++ b/test-requirements.txt
@@ -13,5 +13,5 @@
nosexcover # BSD
oslotest>=1.10.0 # Apache-2.0
oslo.policy>=1.23.0 # Apache-2.0
-oslo.log>=3.22.0 # Apache-2.0
+oslo.log>=3.30.0 # Apache-2.0
tempest>=16.1.0 # Apache-2.0