blob: 9334702d4a574ef40285d7e5670bc0cf122ea1b5 [file] [log] [blame]
DavidPurcellb25f93d2017-01-27 12:46:27 -05001# Copyright 2017 AT&T Corporation.
DavidPurcell029d8c32017-01-06 15:27:41 -05002# All Rights Reserved.
3#
4# Licensed under the Apache License, Version 2.0 (the "License"); you may
5# not use this file except in compliance with the License. You may obtain
6# a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13# License for the specific language governing permissions and limitations
14# under the License.
15
Felipe Monteiro2fe986d2018-03-20 21:53:51 +000016import functools
Felipe Monteirob0595652017-01-23 16:51:58 -050017import logging
Felipe Monteiro8eda8cc2017-03-22 14:15:14 +000018import sys
19
Mykola Yakovlieve0f35502018-09-26 18:26:57 -050020from oslo_log import versionutils
Felipe Monteiro38f344b2017-11-03 12:59:15 +000021from oslo_utils import excutils
Felipe Monteiro8eda8cc2017-03-22 14:15:14 +000022import six
Felipe Monteirob0595652017-01-23 16:51:58 -050023
DavidPurcell029d8c32017-01-06 15:27:41 -050024from tempest import config
Felipe Monteiro51299a12018-06-28 20:03:27 -040025from tempest.lib import exceptions as lib_exc
raiesmh088590c0c2017-03-14 18:06:52 +053026from tempest import test
DavidPurcell029d8c32017-01-06 15:27:41 -050027
Felipe Monteiro88a5bab2017-08-31 04:00:32 +010028from patrole_tempest_plugin import policy_authority
DavidPurcell029d8c32017-01-06 15:27:41 -050029from patrole_tempest_plugin import rbac_exceptions
Rick Bartraed950052017-06-29 17:20:33 -040030from patrole_tempest_plugin import requirements_authority
Doug Schveninger89d9ff82020-08-17 05:59:35 -050031import testtools
32
DavidPurcell029d8c32017-01-06 15:27:41 -050033
34CONF = config.CONF
35LOG = logging.getLogger(__name__)
36
Felipe Monteiro973a1bc2017-06-14 21:23:54 +010037_SUPPORTED_ERROR_CODES = [403, 404]
Cliff Parsons35a77112018-05-07 14:03:40 -050038_DEFAULT_ERROR_CODE = 403
Felipe Monteiro973a1bc2017-06-14 21:23:54 +010039
Sean Pryor7f8993f2017-08-14 12:53:17 -040040RBACLOG = logging.getLogger('rbac_reporting')
41
DavidPurcell029d8c32017-01-06 15:27:41 -050042
Chi Lo8c04bd82018-06-01 16:21:50 -050043def action(service,
Chi Lo8c04bd82018-06-01 16:21:50 -050044 rules=None,
Chi Lo8c04bd82018-06-01 16:21:50 -050045 expected_error_codes=None,
Felipe Monteiro44d77842018-03-21 02:42:59 +000046 extra_target_data=None):
Felipe Monteirof2b58d72017-08-31 22:40:36 +010047 """A decorator for verifying OpenStack policy enforcement.
Felipe Monteirod5d76b82017-03-20 23:18:50 +000048
Felipe Monteiro01d633b2017-08-16 20:17:26 +010049 A decorator which allows for positive and negative RBAC testing. Given:
Rick Bartraed950052017-06-29 17:20:33 -040050
Masayuki Igawa80b9aab2018-01-09 17:00:45 +090051 * an OpenStack service,
52 * a policy action (``rule``) enforced by that service, and
Mykola Yakovlieve0f35502018-09-26 18:26:57 -050053 * the test roles defined by ``[patrole] rbac_test_roles``
Felipe Monteirod5d76b82017-03-20 23:18:50 +000054
Felipe Monteiro01d633b2017-08-16 20:17:26 +010055 determines whether the test role has sufficient permissions to perform an
56 API call that enforces the ``rule``.
Felipe Monteirod5d76b82017-03-20 23:18:50 +000057
Felipe Monteiro01d633b2017-08-16 20:17:26 +010058 This decorator should only be applied to an instance or subclass of
Masayuki Igawa80b9aab2018-01-09 17:00:45 +090059 ``tempest.test.BaseTestCase``.
Felipe Monteiro01d633b2017-08-16 20:17:26 +010060
61 The result from ``_is_authorized`` is used to determine the *expected*
62 test result. The *actual* test result is determined by running the
63 Tempest test this decorator applies to.
64
65 Below are the following possibilities from comparing the *expected* and
66 *actual* results:
67
68 1) If *expected* is True and the test passes (*actual*), this is a success.
69 2) If *expected* is True and the test fails (*actual*), this results in a
Felipe Monteirof16b6b32018-06-28 19:32:59 -040070 ``RbacUnderPermissionException`` exception failure.
Felipe Monteiro01d633b2017-08-16 20:17:26 +010071 3) If *expected* is False and the test passes (*actual*), this results in
Felipe Monteirof16b6b32018-06-28 19:32:59 -040072 an ``RbacOverPermissionException`` exception failure.
Felipe Monteiro01d633b2017-08-16 20:17:26 +010073 4) If *expected* is False and the test fails (*actual*), this is a success.
74
75 As such, negative and positive testing can be applied using this decorator.
76
Felipe Monteiro44d77842018-03-21 02:42:59 +000077 :param str service: An OpenStack service. Examples: "nova" or "neutron".
Felipe Monteiro59f538f2018-08-22 23:34:40 -040078 :param list rules: A list of policy actions defined in a policy file or in
79 code. The rules are logical-ANDed together to derive the expected
Chi Lo8c04bd82018-06-01 16:21:50 -050080 result. Also accepts list of callables that return a policy action.
Felipe Monteiro01d633b2017-08-16 20:17:26 +010081
82 .. note::
83
84 Patrole currently only supports custom JSON policy files.
85
Chi Lo8c04bd82018-06-01 16:21:50 -050086 :type rules: list[str] or list[callable]
Cliff Parsons35a77112018-05-07 14:03:40 -050087 :param list expected_error_codes: When the ``rules`` list parameter is
88 used, then this list indicates the expected error code to use if one
89 of the rules does not allow the role being tested. This list must
90 coincide with and its elements remain in the same order as the rules
91 in the rules list.
92
93 Example::
Felipe Monteiro318fa3b2018-06-19 16:53:33 -040094
Cliff Parsons35a77112018-05-07 14:03:40 -050095 rules=["api_action1", "api_action2"]
96 expected_error_codes=[404, 403]
97
98 a) If api_action1 fails and api_action2 passes, then the expected
99 error code is 404.
100 b) if api_action2 fails and api_action1 passes, then the expected
101 error code is 403.
102 c) if both api_action1 and api_action2 fail, then the expected error
103 code is the first error seen (404).
104
ghanshyam98437d42018-08-17 08:51:43 +0000105 If it is not passed, then it is defaulted to 403.
Cliff Parsons35a77112018-05-07 14:03:40 -0500106
Felipe Monteiro59f538f2018-08-22 23:34:40 -0400107 .. warning::
108
109 A 404 should not be provided *unless* the endpoint masks a
110 ``Forbidden`` exception as a ``NotFound`` exception.
111
112 :type expected_error_codes: list[int]
Felipe Monteiro44d77842018-03-21 02:42:59 +0000113 :param dict extra_target_data: Dictionary, keyed with ``oslo.policy``
114 generic check names, whose values are string literals that reference
115 nested ``tempest.test.BaseTestCase`` attributes. Used by
116 ``oslo.policy`` for performing matching against attributes that are
117 sent along with the API calls. Example::
Felipe Monteiro01d633b2017-08-16 20:17:26 +0100118
119 extra_target_data={
120 "target.token.user_id":
121 "os_alt.auth_provider.credentials.user_id"
122 })
123
Felipe Monteiro51299a12018-06-28 20:03:27 -0400124 :raises RbacInvalidServiceException: If ``service`` is invalid.
Felipe Monteirof16b6b32018-06-28 19:32:59 -0400125 :raises RbacUnderPermissionException: For item (2) above.
126 :raises RbacOverPermissionException: For item (3) above.
127 :raises RbacExpectedWrongException: When a 403 is expected but a 404
128 is raised instead or vice versa.
Felipe Monteiro01d633b2017-08-16 20:17:26 +0100129
130 Examples::
131
132 @rbac_rule_validation.action(
Felipe Monteiro59f538f2018-08-22 23:34:40 -0400133 service="nova",
134 rules=["os_compute_api:os-agents"])
Felipe Monteiro01d633b2017-08-16 20:17:26 +0100135 def test_list_agents_rbac(self):
Felipe Monteiro1c8620a2018-02-25 18:52:22 +0000136 # The call to `override_role` is mandatory.
Sergey Vilgelmd3d77ef2019-02-02 09:34:52 -0600137 with self.override_role():
Felipe Monteiro1c8620a2018-02-25 18:52:22 +0000138 self.agents_client.list_agents()
Felipe Monteirod5d76b82017-03-20 23:18:50 +0000139 """
Felipe Monteiro0854ded2017-05-05 16:30:55 +0100140
141 if extra_target_data is None:
142 extra_target_data = {}
143
Felipe Monteiro59f538f2018-08-22 23:34:40 -0400144 rules, expected_error_codes = _prepare_multi_policy(rules,
Cliff Parsons35a77112018-05-07 14:03:40 -0500145 expected_error_codes)
Felipe Monteiro44d77842018-03-21 02:42:59 +0000146
Sean Pryor7f8993f2017-08-14 12:53:17 -0400147 def decorator(test_func):
Mykola Yakovlieve0f35502018-09-26 18:26:57 -0500148 roles = CONF.patrole.rbac_test_roles
149 # TODO(vegasq) drop once CONF.patrole.rbac_test_role is removed
150 if CONF.patrole.rbac_test_role:
151 msg = ('CONF.patrole.rbac_test_role is deprecated in favor of '
152 'CONF.patrole.rbac_test_roles and will be removed in '
153 'future.')
154 versionutils.report_deprecated_feature(LOG, msg)
155 if not roles:
156 roles.append(CONF.patrole.rbac_test_role)
Felipe Monteiro78fc4892017-04-12 21:33:39 +0100157
Felipe Monteiro2fe986d2018-03-20 21:53:51 +0000158 @functools.wraps(test_func)
DavidPurcell029d8c32017-01-06 15:27:41 -0500159 def wrapper(*args, **kwargs):
Felipe Monteiro78fc4892017-04-12 21:33:39 +0100160 if args and isinstance(args[0], test.BaseTestCase):
161 test_obj = args[0]
162 else:
163 raise rbac_exceptions.RbacResourceSetupFailed(
164 '`rbac_rule_validation` decorator can only be applied to '
165 'an instance of `tempest.test.BaseTestCase`.')
raiesmh088590c0c2017-03-14 18:06:52 +0530166
Felipe Monteiro44d77842018-03-21 02:42:59 +0000167 allowed = True
168 disallowed_rules = []
169 for rule in rules:
170 _allowed = _is_authorized(
171 test_obj, service, rule, extra_target_data)
172 if not _allowed:
173 disallowed_rules.append(rule)
174 allowed = allowed and _allowed
Felipe Monteirod5d76b82017-03-20 23:18:50 +0000175
Cliff Parsons35a77112018-05-07 14:03:40 -0500176 if disallowed_rules:
177 # Choose the first disallowed rule and expect the error
178 # code corresponding to it.
179 first_error_index = rules.index(disallowed_rules[0])
180 exp_error_code = expected_error_codes[first_error_index]
181 LOG.debug("%s: Expecting %d to be raised for policy name: %s",
182 test_func.__name__, exp_error_code,
183 disallowed_rules[0])
Felipe Monteiro59f538f2018-08-22 23:34:40 -0400184 else:
185 exp_error_code = expected_error_codes[0]
Cliff Parsons35a77112018-05-07 14:03:40 -0500186
Rick Bartra12998942017-03-17 17:35:45 -0400187 expected_exception, irregular_msg = _get_exception_type(
Cliff Parsons35a77112018-05-07 14:03:40 -0500188 exp_error_code)
DavidPurcell029d8c32017-01-06 15:27:41 -0500189
Mykola Yakovliev11376ab2018-08-06 15:34:22 -0500190 caught_exception = None
Sean Pryor7f8993f2017-08-14 12:53:17 -0400191 test_status = 'Allowed'
192
DavidPurcell029d8c32017-01-06 15:27:41 -0500193 try:
Sean Pryor7f8993f2017-08-14 12:53:17 -0400194 test_func(*args, **kwargs)
Felipe Monteiro51299a12018-06-28 20:03:27 -0400195 except rbac_exceptions.RbacInvalidServiceException:
196 with excutils.save_and_reraise_exception():
197 msg = ("%s is not a valid service." % service)
198 # FIXME(felipemonteiro): This test_status is logged too
199 # late. Need a function to log it before re-raising.
200 test_status = ('Error, %s' % (msg))
201 LOG.error(msg)
Samantha Blanco36bea052017-07-19 12:01:59 -0400202 except (expected_exception,
Felipe Monteiro74f8e7d2018-09-30 12:33:49 -0400203 rbac_exceptions.BasePatroleResponseBodyException) \
204 as actual_exception:
Mykola Yakovliev11376ab2018-08-06 15:34:22 -0500205 caught_exception = actual_exception
Sean Pryor7f8993f2017-08-14 12:53:17 -0400206 test_status = 'Denied'
Mykola Yakovliev11376ab2018-08-06 15:34:22 -0500207
Felipe Monteiro8eda8cc2017-03-22 14:15:14 +0000208 if irregular_msg:
Felipe Monteiroc0cb7eb2018-06-19 19:50:36 -0400209 LOG.warning(irregular_msg,
210 test_func.__name__,
211 ', '.join(rules),
212 service)
Mykola Yakovliev11376ab2018-08-06 15:34:22 -0500213
DavidPurcell029d8c32017-01-06 15:27:41 -0500214 if allowed:
Mykola Yakovlieve0f35502018-09-26 18:26:57 -0500215 msg = ("User with roles %s was not allowed to perform the "
216 "following actions: %s. Expected allowed actions: "
217 "%s. Expected disallowed actions: %s." % (
218 roles, sorted(rules),
Felipe Monteiro44d77842018-03-21 02:42:59 +0000219 sorted(set(rules) - set(disallowed_rules)),
220 sorted(disallowed_rules)))
DavidPurcell029d8c32017-01-06 15:27:41 -0500221 LOG.error(msg)
Felipe Monteirof16b6b32018-06-28 19:32:59 -0400222 raise rbac_exceptions.RbacUnderPermissionException(
Mykola Yakovliev11376ab2018-08-06 15:34:22 -0500223 "%s Exception was: %s" % (msg, actual_exception))
Felipe Monteirof16b6b32018-06-28 19:32:59 -0400224 except Exception as actual_exception:
Mykola Yakovliev11376ab2018-08-06 15:34:22 -0500225 caught_exception = actual_exception
226
Felipe Monteirof16b6b32018-06-28 19:32:59 -0400227 if _check_for_expected_mismatch_exception(expected_exception,
228 actual_exception):
229 LOG.error('Expected and actual exceptions do not match. '
230 'Expected: %s. Actual: %s.',
231 expected_exception,
232 actual_exception.__class__)
233 raise rbac_exceptions.RbacExpectedWrongException(
234 expected=expected_exception,
235 actual=actual_exception.__class__,
236 exception=actual_exception)
237 else:
238 with excutils.save_and_reraise_exception():
239 exc_info = sys.exc_info()
240 error_details = six.text_type(exc_info[1])
241 msg = ("An unexpected exception has occurred during "
242 "test: %s. Exception was: %s" % (
243 test_func.__name__, error_details))
244 test_status = 'Error, %s' % (error_details)
245 LOG.error(msg)
DavidPurcell029d8c32017-01-06 15:27:41 -0500246 else:
247 if not allowed:
Felipe Monteiro44d77842018-03-21 02:42:59 +0000248 msg = (
249 "OverPermission: Role %s was allowed to perform the "
250 "following disallowed actions: %s" % (
Mykola Yakovlieve0f35502018-09-26 18:26:57 -0500251 roles, sorted(disallowed_rules)
Felipe Monteiro44d77842018-03-21 02:42:59 +0000252 )
253 )
254 LOG.error(msg)
Felipe Monteirof16b6b32018-06-28 19:32:59 -0400255 raise rbac_exceptions.RbacOverPermissionException(msg)
raiesmh088590c0c2017-03-14 18:06:52 +0530256 finally:
Sean Pryor7f8993f2017-08-14 12:53:17 -0400257 if CONF.patrole_log.enable_reporting:
258 RBACLOG.info(
Felipe Monteiroc0cb7eb2018-06-19 19:50:36 -0400259 "[Service]: %s, [Test]: %s, [Rules]: %s, "
Sean Pryor7f8993f2017-08-14 12:53:17 -0400260 "[Expected]: %s, [Actual]: %s",
Felipe Monteiroc0cb7eb2018-06-19 19:50:36 -0400261 service, test_func.__name__, ', '.join(rules),
Sean Pryor7f8993f2017-08-14 12:53:17 -0400262 "Allowed" if allowed else "Denied",
263 test_status)
Felipe Monteiro78fc4892017-04-12 21:33:39 +0100264
Mykola Yakovliev11376ab2018-08-06 15:34:22 -0500265 # Sanity-check that ``override_role`` was called to eliminate
266 # false-positives and bad test flows resulting from exceptions
267 # getting raised too early, too late or not at all, within
268 # the scope of an RBAC test.
269 _validate_override_role_called(
270 test_obj,
271 actual_exception=caught_exception)
272
Felipe Monteiro2fe986d2018-03-20 21:53:51 +0000273 return wrapper
DavidPurcell029d8c32017-01-06 15:27:41 -0500274 return decorator
Rick Bartra12998942017-03-17 17:35:45 -0400275
276
Felipe Monteiro59f538f2018-08-22 23:34:40 -0400277def _prepare_multi_policy(rules, exp_error_codes):
Cliff Parsons35a77112018-05-07 14:03:40 -0500278 if exp_error_codes:
279 if not rules:
280 msg = ("The `rules` list must be provided if using the "
281 "`expected_error_codes` list.")
282 raise ValueError(msg)
283 if len(rules) != len(exp_error_codes):
284 msg = ("The `expected_error_codes` list is not the same length "
285 "as the `rules` list.")
286 raise ValueError(msg)
Cliff Parsons35a77112018-05-07 14:03:40 -0500287 if not isinstance(exp_error_codes, (tuple, list)):
288 exp_error_codes = [exp_error_codes]
289 else:
290 exp_error_codes = []
Cliff Parsons35a77112018-05-07 14:03:40 -0500291
Felipe Monteiro44d77842018-03-21 02:42:59 +0000292 if rules is None:
293 rules = []
294 elif not isinstance(rules, (tuple, list)):
295 rules = [rules]
Cliff Parsons35a77112018-05-07 14:03:40 -0500296
297 # Fill in the exp_error_codes if needed. This is needed for the scenarios
298 # where no exp_error_codes array is provided, so the error codes must be
299 # set to the default error code value and there must be the same number
300 # of error codes as rules.
301 num_ecs = len(exp_error_codes)
302 num_rules = len(rules)
303 if (num_ecs < num_rules):
304 for i in range(num_rules - num_ecs):
305 exp_error_codes.append(_DEFAULT_ERROR_CODE)
306
Chi Lo8c04bd82018-06-01 16:21:50 -0500307 evaluated_rules = [
308 r() if callable(r) else r for r in rules
309 ]
310
311 return evaluated_rules, exp_error_codes
Felipe Monteiro44d77842018-03-21 02:42:59 +0000312
313
Felipe Monteiro318a0bf2018-02-27 06:57:10 -0500314def _is_authorized(test_obj, service, rule, extra_target_data):
Felipe Monteirodea13842017-07-05 04:11:18 +0100315 """Validates whether current RBAC role has permission to do policy action.
316
Felipe Monteirof2b58d72017-08-31 22:40:36 +0100317 :param test_obj: An instance or subclass of ``tempest.test.BaseTestCase``.
Felipe Monteiro01d633b2017-08-16 20:17:26 +0100318 :param service: The OpenStack service that enforces ``rule``.
319 :param rule: The name of the policy action. Examples include
320 "identity:create_user" or "os_compute_api:os-agents".
Felipe Monteirof2b58d72017-08-31 22:40:36 +0100321 :param extra_target_data: Dictionary, keyed with ``oslo.policy`` generic
Felipe Monteiro01d633b2017-08-16 20:17:26 +0100322 check names, whose values are string literals that reference nested
Felipe Monteirof2b58d72017-08-31 22:40:36 +0100323 ``tempest.test.BaseTestCase`` attributes. Used by ``oslo.policy`` for
Felipe Monteiro01d633b2017-08-16 20:17:26 +0100324 performing matching against attributes that are sent along with the API
325 calls.
Sean Pryor7f8993f2017-08-14 12:53:17 -0400326
Felipe Monteiro01d633b2017-08-16 20:17:26 +0100327 :returns: True if the current RBAC role can perform the policy action,
328 else False.
Sean Pryor7f8993f2017-08-14 12:53:17 -0400329
Felipe Monteiro01d633b2017-08-16 20:17:26 +0100330 :raises RbacResourceSetupFailed: If `project_id` or `user_id` are missing
331 from the `auth_provider` attribute in `test_obj`.
Felipe Monteirodea13842017-07-05 04:11:18 +0100332 """
Sean Pryor7f8993f2017-08-14 12:53:17 -0400333
Felipe Monteiro78fc4892017-04-12 21:33:39 +0100334 try:
Felipe Monteiroe8d93e02017-07-19 20:52:20 +0100335 project_id = test_obj.os_primary.credentials.project_id
336 user_id = test_obj.os_primary.credentials.user_id
Felipe Monteiro78fc4892017-04-12 21:33:39 +0100337 except AttributeError as e:
Felipe Monteiroe8d93e02017-07-19 20:52:20 +0100338 msg = ("{0}: project_id or user_id not found in os_primary.credentials"
339 .format(e))
Felipe Monteiro78fc4892017-04-12 21:33:39 +0100340 LOG.error(msg)
341 raise rbac_exceptions.RbacResourceSetupFailed(msg)
342
Mykola Yakovlieve0f35502018-09-26 18:26:57 -0500343 roles = CONF.patrole.rbac_test_roles
344 # TODO(vegasq) drop once CONF.patrole.rbac_test_role is removed
345 if CONF.patrole.rbac_test_role:
346 if not roles:
347 roles.append(CONF.patrole.rbac_test_role)
348
Sergey Vilgelm19e3bec2019-01-07 11:59:41 -0600349 # Adding implied roles
Sergey Vilgelmace8ea32018-11-19 16:25:10 -0600350 roles = test_obj.get_all_needed_roles(roles)
Sergey Vilgelm19e3bec2019-01-07 11:59:41 -0600351
Felipe Monteiro4ef7e532018-03-11 07:17:11 -0400352 # Test RBAC against custom requirements. Otherwise use oslo.policy.
353 if CONF.patrole.test_custom_requirements:
354 authority = requirements_authority.RequirementsAuthority(
355 CONF.patrole.custom_requirements_file, service)
356 else:
357 formatted_target_data = _format_extra_target_data(
358 test_obj, extra_target_data)
Lingxian Kong27f671f2020-12-30 21:23:03 +1300359 policy_authority.PolicyAuthority.os_admin = test_obj.os_admin
Felipe Monteiro4ef7e532018-03-11 07:17:11 -0400360 authority = policy_authority.PolicyAuthority(
361 project_id, user_id, service,
362 extra_target_data=formatted_target_data)
Lingxian Kong27f671f2020-12-30 21:23:03 +1300363
Mykola Yakovlieve0f35502018-09-26 18:26:57 -0500364 is_allowed = authority.allowed(rule, roles)
Felipe Monteiro78fc4892017-04-12 21:33:39 +0100365
Felipe Monteiro4ef7e532018-03-11 07:17:11 -0400366 if is_allowed:
Felipe Monteiroc0cb7eb2018-06-19 19:50:36 -0400367 LOG.debug("[Policy action]: %s, [Role]: %s is allowed!", rule,
Mykola Yakovlieve0f35502018-09-26 18:26:57 -0500368 roles)
Felipe Monteiro4ef7e532018-03-11 07:17:11 -0400369 else:
Felipe Monteiroc0cb7eb2018-06-19 19:50:36 -0400370 LOG.debug("[Policy action]: %s, [Role]: %s is NOT allowed!",
Mykola Yakovlieve0f35502018-09-26 18:26:57 -0500371 rule, roles)
Felipe Monteiro4ef7e532018-03-11 07:17:11 -0400372
373 return is_allowed
Felipe Monteiro78fc4892017-04-12 21:33:39 +0100374
375
Felipe Monteiroc0cb7eb2018-06-19 19:50:36 -0400376def _get_exception_type(expected_error_code=_DEFAULT_ERROR_CODE):
Felipe Monteiro973a1bc2017-06-14 21:23:54 +0100377 """Dynamically calculate the expected exception to be caught.
378
379 Dynamically calculate the expected exception to be caught by the test case.
Felipe Monteirof2b58d72017-08-31 22:40:36 +0100380 Only ``Forbidden`` and ``NotFound`` exceptions are permitted. ``NotFound``
381 is supported because Neutron, for security reasons, masks ``Forbidden``
382 exceptions as ``NotFound`` exceptions.
Felipe Monteiro973a1bc2017-06-14 21:23:54 +0100383
384 :param expected_error_code: the integer representation of the expected
Felipe Monteirof2b58d72017-08-31 22:40:36 +0100385 exception to be caught. Must be contained in
386 ``_SUPPORTED_ERROR_CODES``.
Felipe Monteiro973a1bc2017-06-14 21:23:54 +0100387 :returns: tuple of the exception type corresponding to
Felipe Monteirof2b58d72017-08-31 22:40:36 +0100388 ``expected_error_code`` and a message explaining that a non-Forbidden
Felipe Monteiro973a1bc2017-06-14 21:23:54 +0100389 exception was expected, if applicable.
390 """
Rick Bartra12998942017-03-17 17:35:45 -0400391 expected_exception = None
392 irregular_msg = None
Felipe Monteiro973a1bc2017-06-14 21:23:54 +0100393
394 if not isinstance(expected_error_code, six.integer_types) \
Sean Pryor7f8993f2017-08-14 12:53:17 -0400395 or expected_error_code not in _SUPPORTED_ERROR_CODES:
Felipe Monteiro973a1bc2017-06-14 21:23:54 +0100396 msg = ("Please pass an expected error code. Currently "
397 "supported codes: {0}".format(_SUPPORTED_ERROR_CODES))
398 LOG.error(msg)
399 raise rbac_exceptions.RbacInvalidErrorCode(msg)
Felipe Monteiro78fc4892017-04-12 21:33:39 +0100400
Rick Bartra12998942017-03-17 17:35:45 -0400401 if expected_error_code == 403:
Felipe Monteiro51299a12018-06-28 20:03:27 -0400402 expected_exception = lib_exc.Forbidden
Rick Bartra12998942017-03-17 17:35:45 -0400403 elif expected_error_code == 404:
Felipe Monteiro51299a12018-06-28 20:03:27 -0400404 expected_exception = lib_exc.NotFound
Felipe Monteiroc0cb7eb2018-06-19 19:50:36 -0400405 irregular_msg = ("NotFound exception was caught for test %s. Expected "
406 "policies which may have caused the error: %s. The "
407 "service %s throws a 404 instead of a 403, which is "
Mykola Yakovliev11376ab2018-08-06 15:34:22 -0500408 "irregular")
Rick Bartra12998942017-03-17 17:35:45 -0400409 return expected_exception, irregular_msg
Felipe Monteirofd1db982017-04-13 21:19:41 +0100410
411
412def _format_extra_target_data(test_obj, extra_target_data):
413 """Formats the "extra_target_data" dictionary with correct test data.
414
415 Before being formatted, "extra_target_data" is a dictionary that maps a
Felipe Monteiro01d633b2017-08-16 20:17:26 +0100416 policy string like "trust.trustor_user_id" to a nested list of
Felipe Monteirof2b58d72017-08-31 22:40:36 +0100417 ``tempest.test.BaseTestCase`` attributes. For example, the attribute list
Masayuki Igawa80b9aab2018-01-09 17:00:45 +0900418 in::
Felipe Monteirofd1db982017-04-13 21:19:41 +0100419
Masayuki Igawa80b9aab2018-01-09 17:00:45 +0900420 "trust.trustor_user_id": "os.auth_provider.credentials.user_id"
Felipe Monteirofd1db982017-04-13 21:19:41 +0100421
Felipe Monteiro01d633b2017-08-16 20:17:26 +0100422 is parsed by iteratively calling ``getattr`` until the value of "user_id"
Masayuki Igawa80b9aab2018-01-09 17:00:45 +0900423 is resolved. The resulting dictionary returns::
Felipe Monteirofd1db982017-04-13 21:19:41 +0100424
Masayuki Igawa80b9aab2018-01-09 17:00:45 +0900425 "trust.trustor_user_id": "the user_id of the `os_primary` credential"
Felipe Monteirofd1db982017-04-13 21:19:41 +0100426
Felipe Monteirof2b58d72017-08-31 22:40:36 +0100427 :param test_obj: An instance or subclass of ``tempest.test.BaseTestCase``.
428 :param extra_target_data: Dictionary, keyed with ``oslo.policy`` generic
Felipe Monteiro01d633b2017-08-16 20:17:26 +0100429 check names, whose values are string literals that reference nested
Felipe Monteirof2b58d72017-08-31 22:40:36 +0100430 ``tempest.test.BaseTestCase`` attributes. Used by ``oslo.policy`` for
Felipe Monteiro01d633b2017-08-16 20:17:26 +0100431 performing matching against attributes that are sent along with the API
432 calls.
433 :returns: Dictionary containing additional object data needed by
Felipe Monteirof2b58d72017-08-31 22:40:36 +0100434 ``oslo.policy`` to validate generic checks.
Felipe Monteirofd1db982017-04-13 21:19:41 +0100435 """
436 attr_value = test_obj
437 formatted_target_data = {}
438
439 for user_attribute, attr_string in extra_target_data.items():
440 attrs = attr_string.split('.')
441 for attr in attrs:
442 attr_value = getattr(attr_value, attr)
443 formatted_target_data[user_attribute] = attr_value
444
445 return formatted_target_data
Felipe Monteirof16b6b32018-06-28 19:32:59 -0400446
447
448def _check_for_expected_mismatch_exception(expected_exception,
449 actual_exception):
Mykola Yakovliev11376ab2018-08-06 15:34:22 -0500450 """Checks that ``expected_exception`` matches ``actual_exception``.
451
452 Since Patrole must handle 403/404 it is important that the expected and
453 actual error codes match.
454
455 :param excepted_exception: Expected exception for test.
456 :param actual_exception: Actual exception raised by test.
457 :returns: True if match, else False.
458 :rtype: boolean
459 """
Felipe Monteiro51299a12018-06-28 20:03:27 -0400460 permission_exceptions = (lib_exc.Forbidden, lib_exc.NotFound)
Felipe Monteirof16b6b32018-06-28 19:32:59 -0400461 if isinstance(actual_exception, permission_exceptions):
462 if not isinstance(actual_exception, expected_exception.__class__):
463 return True
464 return False
Mykola Yakovliev11376ab2018-08-06 15:34:22 -0500465
466
467def _validate_override_role_called(test_obj, actual_exception):
Sergey Vilgelm78e7f572019-02-03 10:35:01 -0600468 """Validates that :func:`rbac_utils.RbacUtilsMixin.override_role` is called
Mykola Yakovliev11376ab2018-08-06 15:34:22 -0500469 during each Patrole test.
470
471 Useful for validating that the expected exception isn't raised too early
472 (before ``override_role`` call) or too late (after ``override_call``) or
473 at all (which is a bad test).
474
475 :param test_obj: An instance or subclass of ``tempest.test.BaseTestCase``.
476 :param actual_exception: Actual exception raised by test.
477 :raises RbacOverrideRoleException: If ``override_role`` isn't called, is
478 called too early, or is called too late.
479 """
480 called = test_obj._validate_override_role_called()
481 base_msg = ('This error is unrelated to RBAC and is due to either '
482 'an API or override role failure. Exception: %s' %
483 actual_exception)
484
485 if not called:
486 if actual_exception is not None:
Doug Schveninger89d9ff82020-08-17 05:59:35 -0500487 # Use testtools skipException in base TestCase
488 # to support different skip exceptions used.
489 # Just return so the skip exception will go up
490 # the stack and be handled by the unit testing framework
491 if isinstance(actual_exception,
492 testtools.testcase.TestCase.skipException):
493 return
Mykola Yakovliev11376ab2018-08-06 15:34:22 -0500494 msg = ('Caught exception (%s) but it was raised before the '
495 '`override_role` context. ' % actual_exception.__class__)
496 else:
497 msg = 'Test missing required `override_role` call. '
498 msg += base_msg
499 LOG.error(msg)
500 raise rbac_exceptions.RbacOverrideRoleException(msg)
501 else:
502 exc_caught_in_ctx = test_obj._validate_override_role_caught_exc()
503 # This block is only executed if ``override_role`` is called. If
504 # an exception is raised and the exception wasn't raised in the
505 # ``override_role`` context and if the exception isn't a valid
506 # exception type (instance of ``BasePatroleException``), then this is
507 # a legitimate error.
508 if (not exc_caught_in_ctx and
509 actual_exception is not None and
510 not isinstance(actual_exception,
511 rbac_exceptions.BasePatroleException)):
512 msg = ('Caught exception (%s) but it was raised after the '
513 '`override_role` context. ' % actual_exception.__class__)
514 msg += base_msg
515 LOG.error(msg)
516 raise rbac_exceptions.RbacOverrideRoleException(msg)