Merge "Move virtual interfaces test into misc policy actions file"
diff --git a/patrole_tempest_plugin/config.py b/patrole_tempest_plugin/config.py
index 1180836..a6f30e7 100644
--- a/patrole_tempest_plugin/config.py
+++ b/patrole_tempest_plugin/config.py
@@ -21,33 +21,61 @@
RbacGroup = [
cfg.StrOpt('rbac_test_role',
default='admin',
- help="The current RBAC role against which to run"
- " Patrole tests."),
+ help="""The current RBAC role against which to run Patrole
+tests."""),
cfg.BoolOpt('enable_rbac',
default=True,
help="Enables RBAC tests."),
cfg.BoolOpt('strict_policy_check',
default=False,
- help="If true, throws RbacParsingException for"
- " policies which don't exist. If false, "
- "throws skipException."),
+ help="""If true, throws RbacParsingException for policies which
+don't exist or are not included in the service's policy file. If false, throws
+skipException."""),
# TODO(rb560u): There needs to be support for reading these JSON files from
- # other hosts. It may be possible to leverage the v3 identity policy API
+ # other hosts. It may be possible to leverage the v3 identity policy API.
+ cfg.ListOpt('custom_policy_files',
+ default=['/etc/%s/policy.json'],
+ help="""List of the paths to search for policy files. Each
+policy path assumes that the service name is included in the path once. Also
+assumes Patrole is on the same host as the policy files. The paths should be
+ordered by precedence, with high-priority paths before low-priority paths. The
+first path that is found to contain the service's policy file will be used.
+"""),
cfg.StrOpt('cinder_policy_file',
default='/etc/cinder/policy.json',
- help="Location of the neutron policy file."),
+ help="""Location of the Cinder policy file. Assumed to be on
+the same host as Patrole.""",
+ deprecated_for_removal=True,
+ deprecated_reason="It is better to use `custom_policy_files` "
+ "which supports any OpenStack service."),
cfg.StrOpt('glance_policy_file',
default='/etc/glance/policy.json',
- help="Location of the glance policy file."),
+ help="""Location of the Glance policy file. Assumed to be on
+the same host as Patrole.""",
+ deprecated_for_removal=True,
+ deprecated_reason="It is better to use `custom_policy_files` "
+ "which supports any OpenStack service."),
cfg.StrOpt('keystone_policy_file',
default='/etc/keystone/policy.json',
- help="Location of the keystone policy file."),
+ help="""Location of the custom Keystone policy file. Assumed to
+be on the same host as Patrole.""",
+ deprecated_for_removal=True,
+ deprecated_reason="It is better to use `custom_policy_files` "
+ "which supports any OpenStack service."),
cfg.StrOpt('neutron_policy_file',
default='/etc/neutron/policy.json',
- help="Location of the neutron policy file."),
+ help="""Location of the Neutron policy file. Assumed to be on
+the same host as Patrole.""",
+ deprecated_for_removal=True,
+ deprecated_reason="It is better to use `custom_policy_files` "
+ "which supports any OpenStack service."),
cfg.StrOpt('nova_policy_file',
default='/etc/nova/policy.json',
- help="Location of the nova policy file."),
+ help="""Location of the custom Nova policy file. Assumed to be
+on the same host as Patrole.""",
+ deprecated_for_removal=True,
+ deprecated_reason="It is better to use `custom_policy_files` "
+ "which supports any OpenStack service."),
cfg.BoolOpt('test_custom_requirements',
default=False,
help="""
diff --git a/patrole_tempest_plugin/rbac_exceptions.py b/patrole_tempest_plugin/rbac_exceptions.py
index aa3135e..5ccb216 100644
--- a/patrole_tempest_plugin/rbac_exceptions.py
+++ b/patrole_tempest_plugin/rbac_exceptions.py
@@ -16,25 +16,25 @@
from tempest.lib import exceptions
-class RbacActionFailed (exceptions.ClientRestClientException):
+class RbacActionFailed(exceptions.ClientRestClientException):
message = "Rbac action failed"
-class RbacResourceSetupFailed (exceptions.TempestException):
+class RbacResourceSetupFailed(exceptions.TempestException):
message = "Rbac resource setup failed"
-class RbacOverPermission (exceptions.TempestException):
+class RbacOverPermission(exceptions.TempestException):
message = "Action performed that should not be permitted"
-class RbacInvalidService (exceptions.TempestException):
+class RbacInvalidService(exceptions.TempestException):
message = "Attempted to test an invalid service"
-class RbacParsingException (exceptions.TempestException):
+class RbacParsingException(exceptions.TempestException):
message = "Attempted to test an invalid policy file or action"
-class RbacInvalidErrorCode (exceptions.TempestException):
+class RbacInvalidErrorCode(exceptions.TempestException):
message = "Unsupported error code passed in test"
diff --git a/patrole_tempest_plugin/rbac_policy_parser.py b/patrole_tempest_plugin/rbac_policy_parser.py
index 17a626c..41871cf 100644
--- a/patrole_tempest_plugin/rbac_policy_parser.py
+++ b/patrole_tempest_plugin/rbac_policy_parser.py
@@ -58,26 +58,27 @@
the custom policy file over the default policy implementation is
prioritized.
- :param project_id: type uuid
- :param user_id: type uuid
- :param service: type string
- :param path: type string
+ :param uuid project_id: project_id of object performing API call
+ :param uuid user_id: user_id of object performing API call
+ :param string service: service of the policy file
+ :param dict extra_target_data: dictionary containing additional object
+ data needed by oslo.policy to validate generic checks
"""
if extra_target_data is None:
extra_target_data = {}
- # First check if the service is valid.
self.validate_service(service)
- # Use default path in /etc/<service_name/policy.json if no path
- # is provided.
- path = getattr(CONF.rbac, '%s_policy_file' % str(service), None)
- if not path:
- LOG.info("No config option found for %s,"
- " using default path", str(service))
- path = os.path.join('/etc', service, 'policy.json')
- self.path = path
+ # Prioritize dynamically searching for policy files over relying on
+ # deprecated service-specific policy file locations.
+ if CONF.rbac.custom_policy_files:
+ self.discover_policy_files()
+ self.path = self.policy_files.get(service)
+ else:
+ self.path = getattr(CONF.rbac, '%s_policy_file' % str(service),
+ None)
+
self.rules = policy.Rules.load(self._get_policy_data(service),
'default')
self.project_id = project_id
@@ -86,7 +87,7 @@
@classmethod
def validate_service(cls, service):
- """Validate whether the service passed to ``init`` exists."""
+ """Validate whether the service passed to ``__init__`` exists."""
service = service.lower().strip() if service else None
# Cache the list of available services in memory to avoid needlessly
@@ -102,6 +103,19 @@
raise rbac_exceptions.RbacInvalidService(
"%s is NOT a valid service." % service)
+ @classmethod
+ def discover_policy_files(cls):
+ # Dynamically discover the policy file for each service in
+ # ``cls.available_services``. Pick the first ``candidate_path`` found
+ # out of the potential paths in ``CONF.rbac.custom_policy_files``.
+ if not hasattr(cls, 'policy_files'):
+ cls.policy_files = {}
+ for service in cls.available_services:
+ for candidate_path in CONF.rbac.custom_policy_files:
+ if os.path.isfile(candidate_path % service):
+ cls.policy_files.setdefault(service,
+ candidate_path % service)
+
def allowed(self, rule_name, role):
is_admin_context = self._is_admin_context(role)
is_allowed = self._allowed(
@@ -115,8 +129,8 @@
mgr_policy_data = {}
policy_data = {}
- # Check whether policy file exists.
- if os.path.isfile(self.path):
+ # Check whether policy file exists and attempt to read it.
+ if self.path and os.path.isfile(self.path):
try:
with open(self.path, 'r') as policy_file:
file_policy_data = policy_file.read()
@@ -158,8 +172,11 @@
elif mgr_policy_data:
policy_data = mgr_policy_data
else:
- error_message = 'Policy file for {0} service neither found in '\
- 'code nor at {1}.'.format(service, self.path)
+ error_message = (
+ 'Policy file for {0} service neither found in code nor at {1}.'
+ .format(service, [loc % service for loc in
+ CONF.rbac.custom_policy_files])
+ )
raise rbac_exceptions.RbacParsingException(error_message)
try:
diff --git a/patrole_tempest_plugin/tests/api/compute/test_config_drive_rbac.py b/patrole_tempest_plugin/tests/api/compute/test_config_drive_rbac.py
deleted file mode 100644
index 794e0d2..0000000
--- a/patrole_tempest_plugin/tests/api/compute/test_config_drive_rbac.py
+++ /dev/null
@@ -1,48 +0,0 @@
-# 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 ConfigDriveRbacTest(rbac_base.BaseV2ComputeRbacTest):
-
- @classmethod
- def setup_clients(cls):
- super(ConfigDriveRbacTest, cls).setup_clients()
- cls.client = cls.servers_client
-
- @classmethod
- def skip_checks(cls):
- super(ConfigDriveRbacTest, cls).skip_checks()
- if not test.is_extension_enabled('os-config-drive', 'compute'):
- msg = "%s skipped as os-config-drive extension not enabled." \
- % cls.__name__
- raise cls.skipException(msg)
-
- @decorators.idempotent_id('55c62ef7-b72b-4970-acc6-05b0a4316e5d')
- @rbac_rule_validation.action(
- service="nova",
- rule="os_compute_api:os-config-drive")
- def test_create_test_server_with_config_drive(self):
- self.rbac_utils.switch_role(self, toggle_rbac_role=True)
- # NOTE(felipemonteiro): This policy action is always enforced,
- # regardless whether the config_drive flag is set to true or false.
- # However, it has been explicitly set to true below, in case that this
- # behavior ever changes in the future.
- self.create_test_server(config_drive=True)
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 f361c94..5e92524 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
@@ -23,6 +23,7 @@
from tempest.lib import exceptions as lib_exc
from tempest import test
+from patrole_tempest_plugin import rbac_exceptions
from patrole_tempest_plugin import rbac_rule_validation
from patrole_tempest_plugin.tests.api.compute import rbac_base
@@ -113,6 +114,33 @@
waiters.wait_for_server_status(
self.os_admin.servers_client, self.server_id, 'ACTIVE')
+ @test.requires_ext(extension='os-config-drive', service='compute')
+ @decorators.idempotent_id('2c82e819-382d-4d6f-87f0-a45954cbbc64')
+ @rbac_rule_validation.action(
+ service="nova",
+ rule="os_compute_api:os-config-drive")
+ def test_list_servers_with_details_config_drive(self):
+ """Test list servers with config_drive property in response body."""
+ self.rbac_utils.switch_role(self, toggle_rbac_role=True)
+ body = self.servers_client.list_servers(detail=True)['servers']
+ # If the first server contains "config_drive", then all the others do.
+ if 'config_drive' not in body[0]:
+ raise rbac_exceptions.RbacActionFailed(
+ '"config_drive" attribute not found in response body.')
+
+ @test.requires_ext(extension='os-config-drive', service='compute')
+ @decorators.idempotent_id('55c62ef7-b72b-4970-acc6-05b0a4316e5d')
+ @rbac_rule_validation.action(
+ service="nova",
+ rule="os_compute_api:os-config-drive")
+ def test_show_server_config_drive(self):
+ """Test show server with config_drive property in response body."""
+ self.rbac_utils.switch_role(self, toggle_rbac_role=True)
+ body = self.servers_client.show_server(self.server_id)['server']
+ if 'config_drive' not in body:
+ raise rbac_exceptions.RbacActionFailed(
+ '"config_drive" attribute not found in response body.')
+
@test.requires_ext(extension='os-deferred-delete', service='compute')
@decorators.idempotent_id('189bfed4-1e6d-475c-bb8c-d57e60895391')
@rbac_rule_validation.action(
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 98dbee5..a401f24 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
@@ -30,7 +30,7 @@
@classmethod
def setup_clients(cls):
super(ServerVolumeAttachmentRbacTest, cls).setup_clients()
- cls.volumes_client = cls.os_primary.volumes_client
+ cls.volumes_client = cls.os_primary.volumes_client_latest
@classmethod
def skip_checks(cls):
@@ -68,6 +68,7 @@
@decorators.idempotent_id('997df9c2-6e54-47b6-ab74-e4fdb500f385')
def test_show_volume_attachment(self):
attachment = self.attach_volume(self.server, self.volume)
+
self.rbac_utils.switch_role(self, toggle_rbac_role=True)
self.servers_client.show_volume_attachment(
self.server['id'], attachment['id'])
@@ -80,6 +81,7 @@
def test_update_volume_attachment(self):
attachment = self.attach_volume(self.server, self.volume)
alt_volume = self.create_volume()
+
self.rbac_utils.switch_role(self, toggle_rbac_role=True)
self.servers_client.update_attached_volume(
self.server['id'], attachment['id'], volumeId=alt_volume['id'])
@@ -102,6 +104,7 @@
@decorators.idempotent_id('12b03e90-d087-46af-9c4d-507d021c4984')
def test_delete_volume_attachment(self):
self.attach_volume(self.server, self.volume)
+
self.rbac_utils.switch_role(self, toggle_rbac_role=True)
self.servers_client.detach_volume(self.server['id'], self.volume['id'])
waiters.wait_for_volume_resource_status(self.volumes_client,
diff --git a/patrole_tempest_plugin/tests/api/compute/test_volume_rbac.py b/patrole_tempest_plugin/tests/api/compute/test_volume_rbac.py
index b775872..0935c95 100644
--- a/patrole_tempest_plugin/tests/api/compute/test_volume_rbac.py
+++ b/patrole_tempest_plugin/tests/api/compute/test_volume_rbac.py
@@ -34,6 +34,11 @@
max_microversion = '2.35'
@classmethod
+ def setup_clients(cls):
+ super(VolumeRbacTest, cls).setup_clients()
+ cls.admin_volumes_client = cls.os_admin.volumes_client_latest
+
+ @classmethod
def resource_setup(cls):
super(VolumeRbacTest, cls).resource_setup()
cls.volume = cls.create_volume()
@@ -56,7 +61,7 @@
size=CONF.volume.volume_size)['volume']
# Use the admin volumes client to wait, because waiting involves
# calling show API action which enforces a different policy.
- waiters.wait_for_volume_resource_status(self.os_admin.volumes_client,
+ waiters.wait_for_volume_resource_status(self.admin_volumes_client,
volume['id'], 'available')
# Use non-deprecated volumes_client for deletion.
self.addCleanup(self.volumes_client.delete_volume, volume['id'])
diff --git a/patrole_tempest_plugin/tests/api/identity/v3/test_ep_filter_groups_rbac.py b/patrole_tempest_plugin/tests/api/identity/v3/test_ep_filter_groups_rbac.py
index e7b73b6..00c9f55 100644
--- a/patrole_tempest_plugin/tests/api/identity/v3/test_ep_filter_groups_rbac.py
+++ b/patrole_tempest_plugin/tests/api/identity/v3/test_ep_filter_groups_rbac.py
@@ -73,7 +73,7 @@
@decorators.idempotent_id('5c16368d-1485-4c28-9803-db3fa3510623')
@rbac_rule_validation.action(service="keystone",
- rule="identity:check_endpoint_group")
+ rule="identity:get_endpoint_group")
def test_check_endpoint_group(self):
self.rbac_utils.switch_role(self, toggle_rbac_role=True)
self.endpoint_groups_client.check_endpoint_group(
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 b666a2d..b379c5d 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
@@ -33,6 +33,7 @@
def setup_clients(cls):
super(VolumesActionsRbacTest, cls).setup_clients()
cls.admin_image_client = cls.os_admin.image_client_v2
+ cls.admin_volumes_client = cls.os_admin.volumes_client_latest
@classmethod
def resource_setup(cls):
@@ -54,7 +55,7 @@
server['id'], volumeId=volume_id,
device='/dev/%s' % CONF.compute.volume_device_name)
waiters.wait_for_volume_resource_status(
- self.os_admin.volumes_client, volume_id, 'in-use')
+ self.admin_volumes_client, volume_id, 'in-use')
self.addCleanup(self._detach_volume, volume_id)
def _detach_volume(self, volume_id=None):
@@ -63,7 +64,7 @@
self.volumes_client.detach_volume(volume_id)
waiters.wait_for_volume_resource_status(
- self.os_admin.volumes_client, volume_id, 'available')
+ self.admin_volumes_client, volume_id, 'available')
@test.services('compute')
@rbac_rule_validation.action(service="cinder", rule="volume:attach")
@@ -106,7 +107,7 @@
image_id)
waiters.wait_for_image_status(self.admin_image_client, image_id,
'active')
- waiters.wait_for_volume_resource_status(self.os_admin.volumes_client,
+ waiters.wait_for_volume_resource_status(self.admin_volumes_client,
self.volume['id'], 'available')
@rbac_rule_validation.action(service="cinder",
@@ -161,7 +162,7 @@
self.rbac_utils.switch_role(self, toggle_rbac_role=True)
self.volumes_client.retype_volume(volume['id'], new_type=vol_type)
waiters.wait_for_volume_retype(
- self.os_admin.volumes_client, volume['id'], vol_type)
+ self.admin_volumes_client, volume['id'], vol_type)
@rbac_rule_validation.action(
service="cinder",
@@ -204,7 +205,7 @@
self.volumes_client.force_detach_volume(
volume['id'], connector=None,
attachment_id=attachment['attachment_id'])
- waiters.wait_for_volume_resource_status(self.os_admin.volumes_client,
+ waiters.wait_for_volume_resource_status(self.admin_volumes_client,
volume['id'], 'available')
@@ -221,6 +222,7 @@
def setup_clients(cls):
super(VolumesActionsV310RbacTest, cls).setup_clients()
cls.admin_image_client = cls.os_admin.image_client_v2
+ cls.admin_volumes_client = cls.os_admin.volumes_client_latest
@test.attr(type=["slow"])
@test.services('image')
@@ -243,7 +245,7 @@
image_id)
waiters.wait_for_image_status(self.admin_image_client, image_id,
'active')
- waiters.wait_for_volume_resource_status(self.os_admin.volumes_client,
+ waiters.wait_for_volume_resource_status(self.admin_volumes_client,
volume['id'], 'available')
diff --git a/patrole_tempest_plugin/tests/api/volume/test_volume_quotas_rbac.py b/patrole_tempest_plugin/tests/api/volume/test_volume_quotas_rbac.py
index 6e9812b..851d468 100644
--- a/patrole_tempest_plugin/tests/api/volume/test_volume_quotas_rbac.py
+++ b/patrole_tempest_plugin/tests/api/volume/test_volume_quotas_rbac.py
@@ -29,7 +29,7 @@
@classmethod
def setup_clients(cls):
super(VolumeQuotasRbacTest, cls).setup_clients()
- cls.quotas_client = cls.os_primary.volume_quotas_client
+ cls.quotas_client = cls.os_primary.volume_quotas_v2_client
@rbac_rule_validation.action(service="cinder",
rule="volume_extension:quotas:show")
diff --git a/patrole_tempest_plugin/tests/api/volume/test_volume_transfers_rbac.py b/patrole_tempest_plugin/tests/api/volume/test_volume_transfers_rbac.py
index 212482c..405d02b 100644
--- a/patrole_tempest_plugin/tests/api/volume/test_volume_transfers_rbac.py
+++ b/patrole_tempest_plugin/tests/api/volume/test_volume_transfers_rbac.py
@@ -23,13 +23,11 @@
class VolumesTransfersRbacTest(rbac_base.BaseVolumeRbacTest):
- credentials = ['primary', 'admin']
-
@classmethod
def setup_clients(cls):
super(VolumesTransfersRbacTest, cls).setup_clients()
cls.transfers_client = cls.os_primary.volume_transfers_v2_client
- cls.adm_volumes_client = cls.os_admin.volumes_v2_client
+ cls.admin_volumes_client = cls.os_admin.volumes_client_latest
@classmethod
def resource_setup(cls):
@@ -43,7 +41,7 @@
test_utils.call_and_ignore_notfound_exc(
self.transfers_client.delete_volume_transfer, transfer['id'])
waiters.wait_for_volume_resource_status(
- self.adm_volumes_client, self.volume['id'], 'available')
+ self.admin_volumes_client, self.volume['id'], 'available')
def _create_transfer(self):
transfer = self.transfers_client.create_volume_transfer(
diff --git a/patrole_tempest_plugin/tests/api/volume/test_volumes_backup_rbac.py b/patrole_tempest_plugin/tests/api/volume/test_volumes_backup_rbac.py
index e6944cc..e8949d6 100644
--- a/patrole_tempest_plugin/tests/api/volume/test_volumes_backup_rbac.py
+++ b/patrole_tempest_plugin/tests/api/volume/test_volumes_backup_rbac.py
@@ -41,6 +41,11 @@
if not CONF.volume_feature_enabled.backup:
raise cls.skipException("Cinder backup feature disabled")
+ @classmethod
+ def setup_clients(cls):
+ super(VolumesBackupsRbacTest, cls).setup_clients()
+ cls.admin_backups_client = cls.os_admin.backups_v2_client
+
def _decode_url(self, backup_url):
return json.loads(base64.decode_as_text(backup_url))
@@ -95,7 +100,7 @@
self.rbac_utils.switch_role(self, toggle_rbac_role=True)
self.backups_client.reset_backup_status(backup_id=backup['id'],
status='error')
- waiters.wait_for_volume_resource_status(self.os_admin.backups_client,
+ waiters.wait_for_volume_resource_status(self.admin_backups_client,
backup['id'], 'error')
@test.attr(type=["slow"])
@@ -104,10 +109,11 @@
@decorators.idempotent_id('9c794bf9-2446-4f41-8fe0-80b71e757f9d')
def test_restore_backup(self):
backup = self.create_backup(volume_id=self.volume['id'])
+
self.rbac_utils.switch_role(self, toggle_rbac_role=True)
restore = self.backups_client.restore_backup(backup['id'])['restore']
waiters.wait_for_volume_resource_status(
- self.os_admin.backups_client, restore['backup_id'], 'available')
+ self.admin_backups_client, restore['backup_id'], 'available')
@test.attr(type=["slow"])
@rbac_rule_validation.action(service="cinder",
@@ -121,7 +127,7 @@
volume_id=self.volume['id'])['backup']
self.addCleanup(test_utils.call_and_ignore_notfound_exc,
self.backups_client.delete_backup, backup['id'])
- waiters.wait_for_volume_resource_status(self.os_admin.backups_client,
+ waiters.wait_for_volume_resource_status(self.admin_backups_client,
backup['id'], 'available')
self.rbac_utils.switch_role(self, toggle_rbac_role=True)
diff --git a/patrole_tempest_plugin/tests/api/volume/test_volumes_extend_rbac.py b/patrole_tempest_plugin/tests/api/volume/test_volumes_extend_rbac.py
index 5709669..971e079 100644
--- a/patrole_tempest_plugin/tests/api/volume/test_volumes_extend_rbac.py
+++ b/patrole_tempest_plugin/tests/api/volume/test_volumes_extend_rbac.py
@@ -26,6 +26,11 @@
class VolumesExtendRbacTest(rbac_base.BaseVolumeRbacTest):
@classmethod
+ def setup_clients(cls):
+ super(VolumesExtendRbacTest, cls).setup_clients()
+ cls.admin_volumes_client = cls.os_admin.volumes_client_latest
+
+ @classmethod
def resource_setup(cls):
super(VolumesExtendRbacTest, cls).resource_setup()
# Create a test shared volume for tests
@@ -40,7 +45,7 @@
self.volumes_client.extend_volume(self.volume['id'],
new_size=extend_size)
waiters.wait_for_volume_resource_status(
- self.os_admin.volumes_client, self.volume['id'], 'available')
+ self.admin_volumes_client, self.volume['id'], 'available')
class VolumesExtendV3RbacTest(VolumesExtendRbacTest):
diff --git a/patrole_tempest_plugin/tests/api/volume/test_volumes_manage_rbac.py b/patrole_tempest_plugin/tests/api/volume/test_volumes_manage_rbac.py
index 8e71ee8..bea3a46 100644
--- a/patrole_tempest_plugin/tests/api/volume/test_volumes_manage_rbac.py
+++ b/patrole_tempest_plugin/tests/api/volume/test_volumes_manage_rbac.py
@@ -42,6 +42,7 @@
def setup_clients(cls):
super(VolumesManageRbacTest, cls).setup_clients()
cls.volume_manage_client = cls.os_primary.volume_manage_v2_client
+ cls.admin_volumes_client = cls.os_admin.volumes_client_latest
def _manage_volume(self, org_volume):
# Manage volume
@@ -59,14 +60,14 @@
new_volume_id = self.volume_manage_client.manage_volume(
**new_volume_ref)['volume']['id']
- waiters.wait_for_volume_resource_status(self.os_admin.volumes_client,
+ waiters.wait_for_volume_resource_status(self.admin_volumes_client,
new_volume_id, 'available')
self.addCleanup(self.delete_volume,
self.volumes_client, new_volume_id)
def _unmanage_volume(self, volume):
self.volumes_client.unmanage_volume(volume['id'])
- self.volumes_client.wait_for_resource_deletion(volume['id'])
+ self.admin_volumes_client.wait_for_resource_deletion(volume['id'])
@rbac_rule_validation.action(
service="cinder",
diff --git a/patrole_tempest_plugin/tests/api/volume/test_volumes_snapshots_rbac.py b/patrole_tempest_plugin/tests/api/volume/test_volumes_snapshots_rbac.py
index 422a3db..13ced96 100644
--- a/patrole_tempest_plugin/tests/api/volume/test_volumes_snapshots_rbac.py
+++ b/patrole_tempest_plugin/tests/api/volume/test_volumes_snapshots_rbac.py
@@ -32,6 +32,11 @@
raise cls.skipException("Cinder volume snapshots are disabled")
@classmethod
+ def setup_clients(cls):
+ super(VolumesSnapshotRbacTest, cls).setup_clients()
+ cls.admin_snapshots_client = cls.os_admin.snapshots_v2_client
+
+ @classmethod
def resource_setup(cls):
super(VolumesSnapshotRbacTest, cls).resource_setup()
# Create a test shared volume for tests
@@ -77,8 +82,7 @@
self.snapshots_client.update_snapshot(
self.snapshot['id'], **params)['snapshot']
waiters.wait_for_volume_resource_status(
- self.os_admin.snapshots_client,
- self.snapshot['id'], 'available')
+ self.admin_snapshots_client, self.snapshot['id'], 'available')
@rbac_rule_validation.action(service="cinder",
rule="volume:get_all_snapshots")
@@ -99,7 +103,7 @@
self.rbac_utils.switch_role(self, toggle_rbac_role=True)
# Delete the snapshot
self.snapshots_client.delete_snapshot(temp_snapshot['id'])
- self.os_admin.snapshots_client.wait_for_resource_deletion(
+ self.admin_snapshots_client.wait_for_resource_deletion(
temp_snapshot['id'])
diff --git a/patrole_tempest_plugin/tests/unit/test_rbac_policy_parser.py b/patrole_tempest_plugin/tests/unit/test_rbac_policy_parser.py
index 09de6bf..e3a4429 100644
--- a/patrole_tempest_plugin/tests/unit/test_rbac_policy_parser.py
+++ b/patrole_tempest_plugin/tests/unit/test_rbac_policy_parser.py
@@ -28,12 +28,21 @@
class RbacPolicyTest(base.TestCase):
+ services = {
+ 'services': [
+ {'name': 'custom_rbac_policy'},
+ {'name': 'admin_rbac_policy'},
+ {'name': 'alt_admin_rbac_policy'},
+ {'name': 'tenant_rbac_policy'},
+ {'name': 'test_service'}
+ ]
+ }
+
def setUp(self):
super(RbacPolicyTest, self).setUp()
- self.mock_admin_mgr = mock.patch.object(
- rbac_policy_parser, 'credentials').start()
- self.mock_path = mock.patch.object(
- rbac_policy_parser, 'os').start()
+ mock_admin_mgr = self.patchobject(rbac_policy_parser, 'credentials')
+ mock_admin_mgr.AdminManager().identity_services_v3_client.\
+ list_services.return_value = self.services
current_directory = os.path.dirname(os.path.realpath(__file__))
self.custom_policy_file = os.path.join(current_directory,
@@ -48,35 +57,13 @@
self.tenant_policy_file = os.path.join(current_directory,
'resources',
'tenant_rbac_policy.json')
- services = {
- 'services': [
- {'name': 'cinder', 'links': 'link', 'enabled': True,
- 'type': 'volume', 'id': 'id',
- 'description': 'description'},
- {'name': 'glance', 'links': 'link', 'enabled': True,
- 'type': 'image', 'id': 'id',
- 'description': 'description'},
- {'name': 'nova', 'links': 'link', 'enabled': True,
- 'type': 'compute', 'id': 'id',
- 'description': 'description'},
- {'name': 'keystone', 'links': 'link', 'enabled': True,
- 'type': 'identity', 'id': 'id',
- 'description': 'description'},
- {'name': 'heat', 'links': 'link', 'enabled': True,
- 'type': 'orchestration', 'id': 'id',
- 'description': 'description'},
- {'name': 'neutron', 'links': 'link', 'enabled': True,
- 'type': 'networking', 'id': 'id',
- 'description': 'description'},
- {'name': 'test_service', 'links': 'link', 'enabled': True,
- 'type': 'unit_test', 'id': 'id',
- 'description': 'description'}
- ]
- }
- self.mock_admin_mgr.AdminManager.return_value.\
- identity_services_v3_client.list_services.return_value = \
- services
+ CONF.set_override(
+ 'custom_policy_files',
+ [os.path.join(current_directory, 'resources', '%s.json')],
+ group='rbac')
+ self.addCleanup(CONF.clear_override, 'custom_policy_files',
+ group='rbac')
def _get_fake_policy_rule(self, name, rule):
fake_rule = mock.Mock(check=rule)
@@ -90,9 +77,8 @@
test_tenant_id = mock.sentinel.tenant_id
test_user_id = mock.sentinel.user_id
- self.mock_path.path.join.return_value = self.custom_policy_file
parser = rbac_policy_parser.RbacPolicyParser(
- test_tenant_id, test_user_id, "test_service")
+ test_tenant_id, test_user_id, "custom_rbac_policy")
expected = {
'policy_action_1': ['two', 'four', 'six', 'eight'],
@@ -113,9 +99,8 @@
def test_admin_policy_file_with_admin_role(self):
test_tenant_id = mock.sentinel.tenant_id
test_user_id = mock.sentinel.user_id
- self.mock_path.path.join.return_value = self.admin_policy_file
parser = rbac_policy_parser.RbacPolicyParser(
- test_tenant_id, test_user_id, "test_service")
+ test_tenant_id, test_user_id, "admin_rbac_policy")
role = 'admin'
allowed_rules = [
@@ -134,9 +119,8 @@
def test_admin_policy_file_with_member_role(self):
test_tenant_id = mock.sentinel.tenant_id
test_user_id = mock.sentinel.user_id
- self.mock_path.path.join.return_value = self.admin_policy_file
parser = rbac_policy_parser.RbacPolicyParser(
- test_tenant_id, test_user_id, "test_service")
+ test_tenant_id, test_user_id, "admin_rbac_policy")
role = 'Member'
allowed_rules = [
@@ -153,12 +137,11 @@
allowed = parser.allowed(rule, role)
self.assertFalse(allowed)
- def test_admin_policy_file_with_context_is_admin(self):
+ 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
- self.mock_path.path.join.return_value = self.alt_admin_policy_file
parser = rbac_policy_parser.RbacPolicyParser(
- test_tenant_id, test_user_id, "test_service")
+ test_tenant_id, test_user_id, "alt_admin_rbac_policy")
role = 'fake_admin'
allowed_rules = ['non_admin_rule']
@@ -193,9 +176,8 @@
"""
test_tenant_id = mock.sentinel.tenant_id
test_user_id = mock.sentinel.user_id
- self.mock_path.path.join.return_value = self.tenant_policy_file
parser = rbac_policy_parser.RbacPolicyParser(
- test_tenant_id, test_user_id, "test_service")
+ test_tenant_id, test_user_id, "tenant_rbac_policy")
# Check whether Member role can perform expected actions.
allowed_rules = ['rule1', 'rule2', 'rule3', 'rule4']
@@ -254,7 +236,7 @@
service)
m_log.debug.assert_called_once_with(
- '%s is NOT a valid service.', 'invalid_service')
+ '%s is NOT a valid service.', service)
@mock.patch.object(rbac_policy_parser, 'LOG', autospec=True)
def test_service_is_none_raises_exception(self, m_log):
@@ -274,9 +256,8 @@
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
- self.mock_path.path.join.return_value = self.custom_policy_file
parser = rbac_policy_parser.RbacPolicyParser(
- test_tenant_id, test_user_id, "test_service")
+ test_tenant_id, test_user_id, "custom_rbac_policy")
fake_rule = 'fake_rule'
expected_message = "Policy action: {0} not found in policy file: {1}."\
@@ -291,10 +272,9 @@
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
- self.mock_path.path.join.return_value = self.custom_policy_file
parser = rbac_policy_parser.RbacPolicyParser(
- test_tenant_id, test_user_id, "test_service")
+ test_tenant_id, test_user_id, "custom_rbac_policy")
parser.rules = mock.MagicMock(
**{'__getitem__.return_value.side_effect': Exception(
mock.sentinel.error)})
@@ -327,12 +307,10 @@
test_tenant_id = mock.sentinel.tenant_id
test_user_id = mock.sentinel.user_id
- self.mock_path.path.join.return_value = self.tenant_policy_file
parser = rbac_policy_parser.RbacPolicyParser(
- test_tenant_id, test_user_id, "test_service")
+ test_tenant_id, test_user_id, "tenant_rbac_policy")
policy_data = parser._get_policy_data('fake_service')
-
self.assertIsInstance(policy_data, str)
actual_policy_data = json.loads(policy_data)
@@ -346,7 +324,6 @@
"rule4": "user_id:%(user_id)s",
"admin_tenant_rule": "role:admin and tenant_id:%(tenant_id)s",
"admin_user_rule": "role:admin and user_id:%(user_id)s"
-
}
self.assertEqual(expected_policy_data, actual_policy_data)
@@ -371,12 +348,10 @@
test_tenant_id = mock.sentinel.tenant_id
test_user_id = mock.sentinel.user_id
- self.mock_path.path.join.return_value = self.tenant_policy_file
+
parser = rbac_policy_parser.RbacPolicyParser(
- test_tenant_id, test_user_id, "test_service")
-
+ test_tenant_id, test_user_id, 'tenant_rbac_policy')
policy_data = parser._get_policy_data('fake_service')
-
self.assertIsInstance(policy_data, str)
actual_policy_data = json.loads(policy_data)
@@ -392,23 +367,18 @@
self.assertEqual(expected_policy_data, actual_policy_data)
- @mock.patch.object(rbac_policy_parser, 'credentials', autospec=True)
@mock.patch.object(rbac_policy_parser, 'stevedore', autospec=True)
- def test_get_policy_data_cannot_find_policy(self, mock_stevedore,
- mock_creds):
+ def test_get_policy_data_cannot_find_policy(self, mock_stevedore):
mock_stevedore.named.NamedExtensionManager.return_value = None
- mock_creds.AdminManager.return_value.identity_services_v3_client.\
- list_services.return_value = {
- 'services': [{'name': 'test_service'}]}
- self.mock_path.path.join.return_value = '/etc/test_service/policy.json'
e = self.assertRaises(rbac_exceptions.RbacParsingException,
rbac_policy_parser.RbacPolicyParser,
None, None, 'test_service')
expected_error = \
'Policy file for {0} service neither found in code '\
- 'nor at {1}.'.format('test_service',
- '/etc/test_service/policy.json')
+ 'nor at {1}.'.format(
+ 'test_service',
+ [CONF.rbac.custom_policy_files[0] % 'test_service'])
self.assertIn(expected_error, str(e))
@@ -416,8 +386,6 @@
@mock.patch.object(rbac_policy_parser, 'stevedore', autospec=True)
def test_get_policy_data_without_valid_policy(self, mock_stevedore,
mock_json):
- self.mock_path.path.isfile.return_value = False
-
test_policy_action = mock.Mock(check='rule:bar')
test_policy_action.configure_mock(name='foo')
@@ -450,13 +418,12 @@
mock_json):
mock_stevedore.named.NamedExtensionManager.return_value = None
mock_json.loads.side_effect = ValueError
- self.mock_path.path.join.return_value = self.tenant_policy_file
e = self.assertRaises(rbac_exceptions.RbacParsingException,
rbac_policy_parser.RbacPolicyParser,
- None, None, 'test_service')
+ None, None, 'tenant_rbac_policy')
- expected_error = 'Policy file for {0} service neither found in code '\
- 'nor at {1}.'.format('test_service',
- self.tenant_policy_file)
-
+ expected_error = (
+ 'Policy file for {0} service neither found in code nor at {1}.'
+ .format('tenant_rbac_policy', [CONF.rbac.custom_policy_files[0]
+ % 'tenant_rbac_policy']))
self.assertIn(expected_error, str(e))
diff --git a/releasenotes/notes/dynamic-policy-file-discovery-104cbfc64b55d605.yaml b/releasenotes/notes/dynamic-policy-file-discovery-104cbfc64b55d605.yaml
new file mode 100644
index 0000000..59019cf
--- /dev/null
+++ b/releasenotes/notes/dynamic-policy-file-discovery-104cbfc64b55d605.yaml
@@ -0,0 +1,22 @@
+---
+features:
+ - |
+ Add new configuration option ``[rbac] custom_policy_files``,
+ allowing users to specify list of the paths to search for custom
+ policy files. Each policy path assumes that the service name is
+ included in the path once. Also assumes Patrole is on the same host
+ as the policy files. The paths should be ordered by precedence, with
+ high-priority paths before low-priority paths. The first path that
+ is found to contain the service's policy file will be used.
+deprecations:
+ - |
+ Deprecate the following configuration options from ``[rbac]`` group:
+
+ * cinder_policy_file
+ * glance_policy_file
+ * keystone_policy_file
+ * neutron_policy_file
+ * nova_policy_file
+
+ It is better to use ``[rbac] custom_policy_files`` which supports
+ any OpenStack service.
diff --git a/requirements.txt b/requirements.txt
index 126a3dc..cd6a577 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -7,5 +7,5 @@
oslo.log>=3.22.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>=14.0.0 # Apache-2.0
+tempest>=16.1.0 # Apache-2.0
stevedore>=1.20.0 # Apache-2.0
diff --git a/setup.cfg b/setup.cfg
index 6fd4b8a..212db9e 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -27,6 +27,7 @@
source-dir = doc/source
build-dir = doc/build
all_files = 1
+warning-is-error = 1
[upload_sphinx]
upload-dir = doc/build/html
diff --git a/test-requirements.txt b/test-requirements.txt
index 3e03437..177d0fd 100644
--- a/test-requirements.txt
+++ b/test-requirements.txt
@@ -13,4 +13,4 @@
oslotest>=1.10.0 # Apache-2.0
oslo.policy>=1.23.0 # Apache-2.0
oslo.log>=3.22.0 # Apache-2.0
-tempest>=14.0.0 # Apache-2.0
+tempest>=16.1.0 # Apache-2.0