Merge "Group together tests that create server and require network resources"
diff --git a/patrole_tempest_plugin/config.py b/patrole_tempest_plugin/config.py
index cb00269..1180836 100644
--- a/patrole_tempest_plugin/config.py
+++ b/patrole_tempest_plugin/config.py
@@ -31,6 +31,8 @@
help="If true, throws RbacParsingException for"
" policies which don't exist. 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
cfg.StrOpt('cinder_policy_file',
default='/etc/cinder/policy.json',
help="Location of the neutron policy file."),
@@ -45,5 +47,56 @@
help="Location of the neutron policy file."),
cfg.StrOpt('nova_policy_file',
default='/etc/nova/policy.json',
- help="Location of the nova policy file.")
+ help="Location of the nova policy file."),
+ cfg.BoolOpt('test_custom_requirements',
+ default=False,
+ help="""
+This option determines whether Patrole should run against a
+`custom_requirements_file` which defines RBAC requirements. The
+purpose of setting this flag to True is to verify that RBAC policy
+is in accordance to requirements. The idea is that the
+`custom_requirements_file` perfectly defines what the RBAC requirements are.
+
+Here are the possible outcomes when running the Patrole tests against
+a `custom_requirements_file`:
+
+YAML definition: allowed
+test run: allowed
+test result: pass
+
+YAML definition: allowed
+test run: not allowed
+test result: fail (under-permission)
+
+YAML definition: not allowed
+test run: allowed
+test result: fail (over-permission)
+"""),
+ cfg.StrOpt('custom_requirements_file',
+ help="""
+File path of the yaml file that defines your RBAC requirements. This
+file must be located on the same host that Patrole runs on. The yaml
+file should be written as follows:
+
+```
+<service>:
+ <api_action>:
+ - <allowed_role>
+ - <allowed_role>
+ - <allowed_role>
+ <api_action>:
+ - <allowed_role>
+ - <allowed_role>
+<service>
+ <api_action>:
+ - <allowed_role>
+```
+Where:
+service = the service that is being tested (cinder, nova, etc)
+api_action = the policy action that is being tested. Examples:
+ - volume:create
+ - os_compute_api:servers:start
+ - add_image
+allowed_role = the Keystone role that is allowed to perform the API
+""")
]
diff --git a/patrole_tempest_plugin/rbac_policy_parser.py b/patrole_tempest_plugin/rbac_policy_parser.py
index bb34f6c..17a626c 100644
--- a/patrole_tempest_plugin/rbac_policy_parser.py
+++ b/patrole_tempest_plugin/rbac_policy_parser.py
@@ -25,12 +25,13 @@
from tempest.common import credentials_factory as credentials
from patrole_tempest_plugin import rbac_exceptions
+from patrole_tempest_plugin.rbac_utils import RbacAuthority
CONF = cfg.CONF
LOG = logging.getLogger(__name__)
-class RbacPolicyParser(object):
+class RbacPolicyParser(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 53f84ff..c088ce7 100644
--- a/patrole_tempest_plugin/rbac_rule_validation.py
+++ b/patrole_tempest_plugin/rbac_rule_validation.py
@@ -25,6 +25,7 @@
from patrole_tempest_plugin import rbac_exceptions
from patrole_tempest_plugin import rbac_policy_parser
+from patrole_tempest_plugin import requirements_authority
CONF = config.CONF
LOG = logging.getLogger(__name__)
@@ -39,6 +40,9 @@
A decorator which allows for positive and negative RBAC testing. Given
an OpenStack service and a policy action enforced by that service, an
oslo.policy lookup is performed by calling `authority.get_permission`.
+ Alternatively, the RBAC tests can run against a YAML file that defines
+ policy requirements.
+
The following cases are possible:
* If `allowed` is True and the test passes, this is a success.
@@ -130,6 +134,20 @@
def _is_authorized(test_obj, service, rule_name, extra_target_data):
+ """Validates whether current RBAC role has permission to do policy action.
+
+ :param test_obj: type BaseTestCase (tempest base test class)
+ :param service: the OpenStack service that enforces ``rule_name``
+ :param rule_name: the name of the policy action
+ :param extra_target_data: dictionary with unresolved string literals that
+ reference nested BaseTestCase attributes
+ :returns: True if the current RBAC role can perform the policy action else
+ False
+ :raises RbacParsingException: if ``CONF.rbac.strict_policy_check`` is
+ enabled and the ``rule_name`` does not exist in the system
+ :raises skipException: if ``CONF.rbac.strict_policy_check`` is
+ disabled and the ``rule_name`` does not exist in the system
+ """
try:
project_id = test_obj.auth_provider.credentials.project_id
user_id = test_obj.auth_provider.credentials.user_id
@@ -141,12 +159,17 @@
try:
role = CONF.rbac.rbac_test_role
- formatted_target_data = _format_extra_target_data(
- test_obj, extra_target_data)
- policy_parser = rbac_policy_parser.RbacPolicyParser(
- project_id, user_id, service,
- extra_target_data=formatted_target_data)
- is_allowed = policy_parser.allowed(rule_name, role)
+ # Test RBAC against custom requirements. Otherwise use oslo.policy
+ if CONF.rbac.test_custom_requirements:
+ authority = requirements_authority.RequirementsAuthority(
+ CONF.rbac.custom_requirements_file, service)
+ else:
+ formatted_target_data = _format_extra_target_data(
+ test_obj, extra_target_data)
+ authority = rbac_policy_parser.RbacPolicyParser(
+ project_id, user_id, service,
+ extra_target_data=formatted_target_data)
+ is_allowed = authority.allowed(rule_name, role)
if is_allowed:
LOG.debug("[Action]: %s, [Role]: %s is allowed!", rule_name,
@@ -215,7 +238,8 @@
:param test_obj: type BaseTestCase (tempest base test class)
:param extra_target_data: dictionary with unresolved string literals that
reference nested BaseTestCase attributes
- :returns: dictionary with resolved BaseTestCase attributes
+ :returns: dictionary containing additional object data needed by
+ oslo.policy to validate generic checks
"""
attr_value = test_obj
formatted_target_data = {}
diff --git a/patrole_tempest_plugin/rbac_utils.py b/patrole_tempest_plugin/rbac_utils.py
index 3bb2cbd..00bfd24 100644
--- a/patrole_tempest_plugin/rbac_utils.py
+++ b/patrole_tempest_plugin/rbac_utils.py
@@ -13,6 +13,8 @@
# License for the specific language governing permissions and limitations
# under the License.
+import abc
+import six
import sys
import time
@@ -170,3 +172,13 @@
:returns: True if ``rbac_test_role`` is the admin role.
"""
return CONF.rbac.rbac_test_role == CONF.identity.admin_role
+
+
+@six.add_metaclass(abc.ABCMeta)
+class RbacAuthority(object):
+ # TODO(rb560u): Add documentation explaining what this class is for
+
+ @abc.abstractmethod
+ def allowed(self, rule_name, role):
+ """Determine whether the role should be able to perform the API"""
+ return
diff --git a/patrole_tempest_plugin/requirements_authority.py b/patrole_tempest_plugin/requirements_authority.py
new file mode 100644
index 0000000..2db12db
--- /dev/null
+++ b/patrole_tempest_plugin/requirements_authority.py
@@ -0,0 +1,72 @@
+# Copyright 2017 AT&T Corporation.
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+import yaml
+
+from oslo_log import log as logging
+
+from tempest.lib import exceptions
+
+from patrole_tempest_plugin.rbac_utils import RbacAuthority
+
+LOG = logging.getLogger(__name__)
+
+
+class RequirementsParser(object):
+ _inner = None
+
+ class Inner(object):
+ _rbac_map = None
+
+ def __init__(self, filepath):
+ with open(filepath) as f:
+ RequirementsParser.Inner._rbac_map = \
+ list(yaml.safe_load_all(f))
+
+ def __init__(self, filepath):
+ if RequirementsParser._inner is None:
+ RequirementsParser._inner = RequirementsParser.Inner(filepath)
+
+ @staticmethod
+ def parse(component):
+ try:
+ for section in RequirementsParser.Inner._rbac_map:
+ if component in section:
+ return section[component]
+ except yaml.parser.ParserError:
+ LOG.error("Error while parsing the requirements YAML file. Did "
+ "you pass a valid component name from the test case?")
+ return None
+
+
+class RequirementsAuthority(RbacAuthority):
+ def __init__(self, filepath=None, component=None):
+ if filepath is not None and component is not None:
+ self.roles_dict = RequirementsParser(filepath).parse(component)
+ else:
+ self.roles_dict = None
+
+ def allowed(self, rule_name, role):
+ if self.roles_dict is None:
+ raise exceptions.InvalidConfiguration(
+ "Roles dictionary parsed from requirements YAML file is "
+ "empty. Ensure the requirements YAML file is correctly "
+ "formatted.")
+ try:
+ _api = self.roles_dict[rule_name]
+ return role in _api
+ except KeyError:
+ raise KeyError("'%s' API is not defined in the requirements YAML "
+ "file" % rule_name)
+ return False
diff --git a/patrole_tempest_plugin/tests/api/compute/rbac_base.py b/patrole_tempest_plugin/tests/api/compute/rbac_base.py
index 0be42e7..0a3a1f5 100644
--- a/patrole_tempest_plugin/tests/api/compute/rbac_base.py
+++ b/patrole_tempest_plugin/tests/api/compute/rbac_base.py
@@ -37,7 +37,9 @@
super(BaseV2ComputeRbacTest, cls).setup_clients()
cls.auth_provider = cls.os_primary.auth_provider
cls.rbac_utils = rbac_utils.RbacUtils(cls)
+
cls.hosts_client = cls.os_primary.hosts_client
+ cls.tenant_usages_client = cls.os_primary.tenant_usages_client
@classmethod
def resource_setup(cls):
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 aea018a..afa8c02 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
@@ -211,12 +211,34 @@
def test_show_server_usage(self):
"""Test show server usage, part of os-server-usage.
- TODO(felipemonteiro): Once multiple policy test is supported, this
+ TODO(felipemonteiro): Once multiple policy testing is supported, this
test can be combined with the generic test for showing a server.
"""
self.rbac_utils.switch_role(self, toggle_rbac_role=True)
self.servers_client.show_server(self.server_id)
+ @test.requires_ext(extension='os-simple-tenant-usage', service='compute')
+ @rbac_rule_validation.action(
+ service="nova",
+ rule="os_compute_api:os-simple-tenant-usage:list")
+ @decorators.idempotent_id('2aef094f-0452-4df6-a66a-0ec22a92b16e')
+ def test_list_simple_tenant_usages(self):
+ """Test list tenant usages, part of os-simple-tenant-usage."""
+ self.rbac_utils.switch_role(self, toggle_rbac_role=True)
+ self.tenant_usages_client.list_tenant_usages()
+
+ @test.requires_ext(extension='os-simple-tenant-usage', service='compute')
+ @rbac_rule_validation.action(
+ service="nova",
+ rule="os_compute_api:os-simple-tenant-usage:show")
+ @decorators.idempotent_id('fe7eacda-15c4-4bf7-93ef-1091c4546a9d')
+ def test_show_simple_tenant_usage(self):
+ """Test show tenant usage, part of os-simple-tenant-usage."""
+ tenant_id = self.auth_provider.credentials.tenant_id
+
+ self.rbac_utils.switch_role(self, toggle_rbac_role=True)
+ self.tenant_usages_client.show_tenant_usage(tenant_id=tenant_id)
+
@testtools.skipUnless(CONF.compute_feature_enabled.suspend,
"Suspend compute feature is not available.")
@decorators.idempotent_id('b775930f-237c-431c-83ae-d33ed1b9700b')
diff --git a/patrole_tempest_plugin/tests/api/compute/test_simple_tenant_usage_rbac.py b/patrole_tempest_plugin/tests/api/compute/test_simple_tenant_usage_rbac.py
deleted file mode 100644
index ae2a0fd..0000000
--- a/patrole_tempest_plugin/tests/api/compute/test_simple_tenant_usage_rbac.py
+++ /dev/null
@@ -1,56 +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 SimpleTenantUsageRbacTest(rbac_base.BaseV2ComputeRbacTest):
-
- @classmethod
- def setup_clients(cls):
- super(SimpleTenantUsageRbacTest, cls).setup_clients()
- cls.tenant_usages_client = cls.os_primary.tenant_usages_client
-
- @classmethod
- def skip_checks(cls):
- super(SimpleTenantUsageRbacTest, cls).skip_checks()
- if not test.is_extension_enabled('os-simple-tenant-usage', 'compute'):
- msg = ("%s skipped as os-simple-tenant-usage not "
- "enabled." % cls.__name__)
- raise cls.skipException(msg)
-
- @rbac_rule_validation.action(
- service="nova",
- rule="os_compute_api:os-simple-tenant-usage:list")
- @decorators.idempotent_id('2aef094f-0452-4df6-a66a-0ec22a92b16e')
- def test_simple_tenant_usage_list(self):
- self.rbac_utils.switch_role(self, toggle_rbac_role=True)
- self.tenant_usages_client.list_tenant_usages()
-
- @rbac_rule_validation.action(
- service="nova",
- rule="os_compute_api:os-simple-tenant-usage:show")
- @decorators.idempotent_id('fe7eacda-15c4-4bf7-93ef-1091c4546a9d')
- def test_simple_tenant_usage_show(self):
- # A server must be created in order for usage activity to exist; else
- # the validation method in the API call throws an error.
- self.create_test_server(wait_until='ACTIVE')['id']
- tenant_id = self.auth_provider.credentials.tenant_id
- self.rbac_utils.switch_role(self, toggle_rbac_role=True)
- self.tenant_usages_client.show_tenant_usage(tenant_id=tenant_id)
diff --git a/patrole_tempest_plugin/tests/api/identity/v3/test_trusts_rbac.py b/patrole_tempest_plugin/tests/api/identity/v3/test_trusts_rbac.py
index 763c407..c25af25 100644
--- a/patrole_tempest_plugin/tests/api/identity/v3/test_trusts_rbac.py
+++ b/patrole_tempest_plugin/tests/api/identity/v3/test_trusts_rbac.py
@@ -68,7 +68,8 @@
service="keystone",
rule="identity:create_trust",
extra_target_data={
- "trust.trustor_user_id": "os.auth_provider.credentials.user_id"
+ "trust.trustor_user_id":
+ "os_primary.auth_provider.credentials.user_id"
})
def test_create_trust(self):
self.rbac_utils.switch_role(self, toggle_rbac_role=True)
diff --git a/patrole_tempest_plugin/tests/unit/resources/rbac_roles.yaml b/patrole_tempest_plugin/tests/unit/resources/rbac_roles.yaml
new file mode 100644
index 0000000..c5436d0
--- /dev/null
+++ b/patrole_tempest_plugin/tests/unit/resources/rbac_roles.yaml
@@ -0,0 +1,6 @@
+Test:
+ test:create:
+ - test_member
+ - _member_
+ test:create2:
+ - test_member
diff --git a/patrole_tempest_plugin/tests/unit/test_requirements_authority.py b/patrole_tempest_plugin/tests/unit/test_requirements_authority.py
new file mode 100644
index 0000000..1fb9636
--- /dev/null
+++ b/patrole_tempest_plugin/tests/unit/test_requirements_authority.py
@@ -0,0 +1,85 @@
+# Copyright 2017 AT&T Corporation.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import os
+
+from tempest.lib import exceptions
+from tempest.tests import base
+
+from patrole_tempest_plugin import requirements_authority as req_auth
+
+
+class RequirementsAuthorityTest(base.TestCase):
+ def setUp(self):
+ super(RequirementsAuthorityTest, self).setUp()
+ self.rbac_auth = req_auth.RequirementsAuthority()
+ self.current_directory = os.path.dirname(os.path.realpath(__file__))
+ self.yaml_test_file = os.path.join(self.current_directory,
+ 'resources',
+ 'rbac_roles.yaml')
+ self.expected_result = {'test:create': ['test_member', '_member_'],
+ 'test:create2': ['test_member']}
+
+ def test_requirements_auth_init(self):
+ rbac_auth = req_auth.RequirementsAuthority(self.yaml_test_file, 'Test')
+ self.assertEqual(self.expected_result, rbac_auth.roles_dict)
+
+ def test_auth_allowed_empty_roles(self):
+ self.rbac_auth.roles_dict = None
+ self.assertRaises(exceptions.InvalidConfiguration,
+ self.rbac_auth.allowed, "", "")
+
+ def test_auth_allowed_role_in_api(self):
+ self.rbac_auth.roles_dict = {'api': ['_member_']}
+ self.assertTrue(self.rbac_auth.allowed("api", "_member_"))
+
+ def test_auth_allowed_role_not_in_api(self):
+ self.rbac_auth.roles_dict = {'api': ['_member_']}
+ self.assertFalse(self.rbac_auth.allowed("api", "support_member"))
+
+ def test_parser_get_allowed_except_keyerror(self):
+ self.rbac_auth.roles_dict = {}
+ self.assertRaises(KeyError, self.rbac_auth.allowed,
+ "api", "support_member")
+
+ def test_parser_init(self):
+ req_auth.RequirementsParser(self.yaml_test_file)
+ self.assertEqual([{'Test': self.expected_result}],
+ req_auth.RequirementsParser.Inner._rbac_map)
+
+ def test_parser_role_in_api(self):
+ req_auth.RequirementsParser.Inner._rbac_map = \
+ [{'Test': self.expected_result}]
+ self.rbac_auth.roles_dict = req_auth.RequirementsParser.parse("Test")
+
+ self.assertEqual(self.expected_result, self.rbac_auth.roles_dict)
+ self.assertTrue(self.rbac_auth.allowed("test:create2", "test_member"))
+
+ def test_parser_role_not_in_api(self):
+ req_auth.RequirementsParser.Inner._rbac_map = \
+ [{'Test': self.expected_result}]
+ self.rbac_auth.roles_dict = req_auth.RequirementsParser.parse("Test")
+
+ self.assertEqual(self.expected_result, self.rbac_auth.roles_dict)
+ self.assertFalse(self.rbac_auth.allowed("test:create2", "_member_"))
+
+ def test_parser_except_invalid_configuration(self):
+ req_auth.RequirementsParser.Inner._rbac_map = \
+ [{'Test': self.expected_result}]
+ self.rbac_auth.roles_dict = \
+ req_auth.RequirementsParser.parse("Failure")
+
+ self.assertIsNone(self.rbac_auth.roles_dict)
+ self.assertRaises(exceptions.InvalidConfiguration,
+ self.rbac_auth.allowed, "", "")
diff --git a/releasenotes/notes/support_requirements_yaml-a90e0188a19421ba.yaml b/releasenotes/notes/support_requirements_yaml-a90e0188a19421ba.yaml
new file mode 100644
index 0000000..d2f5519
--- /dev/null
+++ b/releasenotes/notes/support_requirements_yaml-a90e0188a19421ba.yaml
@@ -0,0 +1,12 @@
+---
+features:
+ - |
+ Add support of running Patrole against a custom requirements YAML that
+ defines RBAC requirements. The YAML file lists all the APIs and the roles
+ that should have access to the APIs. The purpose of running Patrole against
+ a requirements YAML is to verify that the RBAC policy is in accordance to
+ deployment specific requirements. Running Patrole against a requirements
+ YAML is completely optional and can be enabled by setting the
+ ``[rbac] test_custom_requirements`` option to True in Tempest's
+ configuration file. The requirements YAML must be located on the same host
+ that Patrole runs on.
diff --git a/requirements.txt b/requirements.txt
index 6871057..126a3dc 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -2,10 +2,10 @@
# of appearance. Changing the order has an impact on the overall integration
# process, which may cause wedges in the gate later.
hacking!=0.13.0,<0.14,>=0.12.0 # Apache-2.0
-pbr>=1.8 # Apache-2.0
-urllib3>=1.15.1 # MIT
-oslo.log>=3.11.0 # Apache-2.0
-oslo.config>=3.22.0 # Apache-2.0
-oslo.policy>=1.17.0 # Apache-2.0
-tempest>=14.0.0 # Apache-2.0
-stevedore>=1.20.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.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
+stevedore>=1.20.0 # Apache-2.0
diff --git a/setup.py b/setup.py
index f730546..566d844 100644
--- a/setup.py
+++ b/setup.py
@@ -1,5 +1,4 @@
-# Copyright 2017 ATT Corporation.
-# All Rights Reserved.
+# Copyright (c) 2013 Hewlett-Packard Development Company, L.P.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -26,5 +25,5 @@
pass
setuptools.setup(
- setup_requires=['pbr>=1.8'],
+ setup_requires=['pbr>=2.0.0'],
pbr=True)
diff --git a/test-requirements.txt b/test-requirements.txt
index 0639513..3e03437 100644
--- a/test-requirements.txt
+++ b/test-requirements.txt
@@ -1,16 +1,16 @@
# The order of packages is significant, because pip processes them in the order
# of appearance. Changing the order has an impact on the overall integration
# process, which may cause wedges in the gate later.
-hacking>=0.12.0,!=0.13.0,<0.14 # Apache-2.0
+hacking!=0.13.0,<0.14,>=0.12.0 # Apache-2.0
-sphinx>=1.2.1,!=1.3b1,<1.4 # BSD
+sphinx>=1.6.2 # BSD
openstackdocstheme>=1.11.0 # Apache-2.0
-reno>=1.8.0 # Apache-2.0
+reno!=2.3.1,>=1.8.0 # Apache-2.0
mock>=2.0 # BSD
-coverage>=4.0 # Apache-2.0
+coverage!=4.4,>=4.0 # Apache-2.0
nose # LGPL
nosexcover # BSD
oslotest>=1.10.0 # Apache-2.0
-oslo.policy>=1.17.0 # Apache-2.0
-oslo.log>=3.11.0 # Apache-2.0
-tempest>=12.1.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