blob: a7927fccae7ac6675e803b68805eaa607b640e30 [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
Felipe Monteiro44d77842018-03-21 02:42:59 +000020from 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
DavidPurcell029d8c32017-01-06 15:27:41 -050031
32CONF = config.CONF
33LOG = logging.getLogger(__name__)
34
Felipe Monteiro973a1bc2017-06-14 21:23:54 +010035_SUPPORTED_ERROR_CODES = [403, 404]
Cliff Parsons35a77112018-05-07 14:03:40 -050036_DEFAULT_ERROR_CODE = 403
Felipe Monteiro973a1bc2017-06-14 21:23:54 +010037
Sean Pryor7f8993f2017-08-14 12:53:17 -040038RBACLOG = logging.getLogger('rbac_reporting')
39
DavidPurcell029d8c32017-01-06 15:27:41 -050040
Cliff Parsons35a77112018-05-07 14:03:40 -050041def action(service, rule='', rules=None,
42 expected_error_code=_DEFAULT_ERROR_CODE, expected_error_codes=None,
Felipe Monteiro44d77842018-03-21 02:42:59 +000043 extra_target_data=None):
Felipe Monteirof2b58d72017-08-31 22:40:36 +010044 """A decorator for verifying OpenStack policy enforcement.
Felipe Monteirod5d76b82017-03-20 23:18:50 +000045
Felipe Monteiro01d633b2017-08-16 20:17:26 +010046 A decorator which allows for positive and negative RBAC testing. Given:
Rick Bartraed950052017-06-29 17:20:33 -040047
Masayuki Igawa80b9aab2018-01-09 17:00:45 +090048 * an OpenStack service,
49 * a policy action (``rule``) enforced by that service, and
50 * the test role defined by ``[patrole] rbac_test_role``
Felipe Monteirod5d76b82017-03-20 23:18:50 +000051
Felipe Monteiro01d633b2017-08-16 20:17:26 +010052 determines whether the test role has sufficient permissions to perform an
53 API call that enforces the ``rule``.
Felipe Monteirod5d76b82017-03-20 23:18:50 +000054
Felipe Monteiro01d633b2017-08-16 20:17:26 +010055 This decorator should only be applied to an instance or subclass of
Masayuki Igawa80b9aab2018-01-09 17:00:45 +090056 ``tempest.test.BaseTestCase``.
Felipe Monteiro01d633b2017-08-16 20:17:26 +010057
58 The result from ``_is_authorized`` is used to determine the *expected*
59 test result. The *actual* test result is determined by running the
60 Tempest test this decorator applies to.
61
62 Below are the following possibilities from comparing the *expected* and
63 *actual* results:
64
65 1) If *expected* is True and the test passes (*actual*), this is a success.
66 2) If *expected* is True and the test fails (*actual*), this results in a
Felipe Monteirof16b6b32018-06-28 19:32:59 -040067 ``RbacUnderPermissionException`` exception failure.
Felipe Monteiro01d633b2017-08-16 20:17:26 +010068 3) If *expected* is False and the test passes (*actual*), this results in
Felipe Monteirof16b6b32018-06-28 19:32:59 -040069 an ``RbacOverPermissionException`` exception failure.
Felipe Monteiro01d633b2017-08-16 20:17:26 +010070 4) If *expected* is False and the test fails (*actual*), this is a success.
71
72 As such, negative and positive testing can be applied using this decorator.
73
Felipe Monteiro44d77842018-03-21 02:42:59 +000074 :param str service: An OpenStack service. Examples: "nova" or "neutron".
75 :param str rule: (DEPRECATED) A policy action defined in a policy.json file
76 or in code.
77 :param list rules: A list of policy actions defined in a policy.json file
78 or in code. The rules are logical-ANDed together to derive the expected
79 result.
Felipe Monteiro01d633b2017-08-16 20:17:26 +010080
81 .. note::
82
83 Patrole currently only supports custom JSON policy files.
84
Felipe Monteiro318fa3b2018-06-19 16:53:33 -040085 :param int expected_error_code: (DEPRECATED) Overrides default value of 403
86 (Forbidden) with endpoint-specific error code. Currently only supports
87 403 and 404. Support for 404 is needed because some services, like
88 Neutron, intentionally throw a 404 for security reasons.
Felipe Monteirod5d76b82017-03-20 23:18:50 +000089
Felipe Monteiro01d633b2017-08-16 20:17:26 +010090 .. warning::
91
92 A 404 should not be provided *unless* the endpoint masks a
Felipe Monteirof2b58d72017-08-31 22:40:36 +010093 ``Forbidden`` exception as a ``NotFound`` exception.
Felipe Monteiro01d633b2017-08-16 20:17:26 +010094
Cliff Parsons35a77112018-05-07 14:03:40 -050095 :param list expected_error_codes: When the ``rules`` list parameter is
96 used, then this list indicates the expected error code to use if one
97 of the rules does not allow the role being tested. This list must
98 coincide with and its elements remain in the same order as the rules
99 in the rules list.
100
101 Example::
Felipe Monteiro318fa3b2018-06-19 16:53:33 -0400102
Cliff Parsons35a77112018-05-07 14:03:40 -0500103 rules=["api_action1", "api_action2"]
104 expected_error_codes=[404, 403]
105
106 a) If api_action1 fails and api_action2 passes, then the expected
107 error code is 404.
108 b) if api_action2 fails and api_action1 passes, then the expected
109 error code is 403.
110 c) if both api_action1 and api_action2 fail, then the expected error
111 code is the first error seen (404).
112
ghanshyam98437d42018-08-17 08:51:43 +0000113 If it is not passed, then it is defaulted to 403.
Cliff Parsons35a77112018-05-07 14:03:40 -0500114
Felipe Monteiro44d77842018-03-21 02:42:59 +0000115 :param dict extra_target_data: Dictionary, keyed with ``oslo.policy``
116 generic check names, whose values are string literals that reference
117 nested ``tempest.test.BaseTestCase`` attributes. Used by
118 ``oslo.policy`` for performing matching against attributes that are
119 sent along with the API calls. Example::
Felipe Monteiro01d633b2017-08-16 20:17:26 +0100120
121 extra_target_data={
122 "target.token.user_id":
123 "os_alt.auth_provider.credentials.user_id"
124 })
125
Felipe Monteiro51299a12018-06-28 20:03:27 -0400126 :raises RbacInvalidServiceException: If ``service`` is invalid.
Felipe Monteirof16b6b32018-06-28 19:32:59 -0400127 :raises RbacUnderPermissionException: For item (2) above.
128 :raises RbacOverPermissionException: For item (3) above.
129 :raises RbacExpectedWrongException: When a 403 is expected but a 404
130 is raised instead or vice versa.
Felipe Monteiro01d633b2017-08-16 20:17:26 +0100131
132 Examples::
133
134 @rbac_rule_validation.action(
135 service="nova", rule="os_compute_api:os-agents")
136 def test_list_agents_rbac(self):
Felipe Monteiro1c8620a2018-02-25 18:52:22 +0000137 # The call to `override_role` is mandatory.
138 with self.rbac_utils.override_role(self):
139 self.agents_client.list_agents()
Felipe Monteirod5d76b82017-03-20 23:18:50 +0000140 """
Felipe Monteiro0854ded2017-05-05 16:30:55 +0100141
142 if extra_target_data is None:
143 extra_target_data = {}
144
Cliff Parsons35a77112018-05-07 14:03:40 -0500145 rules, expected_error_codes = _prepare_multi_policy(rule, rules,
146 expected_error_code,
147 expected_error_codes)
Felipe Monteiro44d77842018-03-21 02:42:59 +0000148
Sean Pryor7f8993f2017-08-14 12:53:17 -0400149 def decorator(test_func):
Felipe Monteirof6eb8622017-08-06 06:08:02 +0100150 role = CONF.patrole.rbac_test_role
Felipe Monteiro78fc4892017-04-12 21:33:39 +0100151
Felipe Monteiro2fe986d2018-03-20 21:53:51 +0000152 @functools.wraps(test_func)
DavidPurcell029d8c32017-01-06 15:27:41 -0500153 def wrapper(*args, **kwargs):
Felipe Monteiro78fc4892017-04-12 21:33:39 +0100154 if args and isinstance(args[0], test.BaseTestCase):
155 test_obj = args[0]
156 else:
157 raise rbac_exceptions.RbacResourceSetupFailed(
158 '`rbac_rule_validation` decorator can only be applied to '
159 'an instance of `tempest.test.BaseTestCase`.')
raiesmh088590c0c2017-03-14 18:06:52 +0530160
Felipe Monteiro44d77842018-03-21 02:42:59 +0000161 allowed = True
162 disallowed_rules = []
163 for rule in rules:
164 _allowed = _is_authorized(
165 test_obj, service, rule, extra_target_data)
166 if not _allowed:
167 disallowed_rules.append(rule)
168 allowed = allowed and _allowed
Felipe Monteirod5d76b82017-03-20 23:18:50 +0000169
Cliff Parsons35a77112018-05-07 14:03:40 -0500170 exp_error_code = expected_error_code
171 if disallowed_rules:
172 # Choose the first disallowed rule and expect the error
173 # code corresponding to it.
174 first_error_index = rules.index(disallowed_rules[0])
175 exp_error_code = expected_error_codes[first_error_index]
176 LOG.debug("%s: Expecting %d to be raised for policy name: %s",
177 test_func.__name__, exp_error_code,
178 disallowed_rules[0])
179
Rick Bartra12998942017-03-17 17:35:45 -0400180 expected_exception, irregular_msg = _get_exception_type(
Cliff Parsons35a77112018-05-07 14:03:40 -0500181 exp_error_code)
DavidPurcell029d8c32017-01-06 15:27:41 -0500182
Mykola Yakovliev11376ab2018-08-06 15:34:22 -0500183 caught_exception = None
Sean Pryor7f8993f2017-08-14 12:53:17 -0400184 test_status = 'Allowed'
185
DavidPurcell029d8c32017-01-06 15:27:41 -0500186 try:
Sean Pryor7f8993f2017-08-14 12:53:17 -0400187 test_func(*args, **kwargs)
Felipe Monteiro51299a12018-06-28 20:03:27 -0400188 except rbac_exceptions.RbacInvalidServiceException:
189 with excutils.save_and_reraise_exception():
190 msg = ("%s is not a valid service." % service)
191 # FIXME(felipemonteiro): This test_status is logged too
192 # late. Need a function to log it before re-raising.
193 test_status = ('Error, %s' % (msg))
194 LOG.error(msg)
Samantha Blanco36bea052017-07-19 12:01:59 -0400195 except (expected_exception,
196 rbac_exceptions.RbacConflictingPolicies,
Mykola Yakovliev11376ab2018-08-06 15:34:22 -0500197 rbac_exceptions.RbacMalformedResponse) as actual_exception:
198 caught_exception = actual_exception
Sean Pryor7f8993f2017-08-14 12:53:17 -0400199 test_status = 'Denied'
Mykola Yakovliev11376ab2018-08-06 15:34:22 -0500200
Felipe Monteiro8eda8cc2017-03-22 14:15:14 +0000201 if irregular_msg:
Felipe Monteiroc0cb7eb2018-06-19 19:50:36 -0400202 LOG.warning(irregular_msg,
203 test_func.__name__,
204 ', '.join(rules),
205 service)
Mykola Yakovliev11376ab2018-08-06 15:34:22 -0500206
DavidPurcell029d8c32017-01-06 15:27:41 -0500207 if allowed:
Felipe Monteiro44d77842018-03-21 02:42:59 +0000208 msg = ("Role %s was not allowed to perform the following "
209 "actions: %s. Expected allowed actions: %s. "
210 "Expected disallowed actions: %s." % (
211 role, sorted(rules),
212 sorted(set(rules) - set(disallowed_rules)),
213 sorted(disallowed_rules)))
DavidPurcell029d8c32017-01-06 15:27:41 -0500214 LOG.error(msg)
Felipe Monteirof16b6b32018-06-28 19:32:59 -0400215 raise rbac_exceptions.RbacUnderPermissionException(
Mykola Yakovliev11376ab2018-08-06 15:34:22 -0500216 "%s Exception was: %s" % (msg, actual_exception))
Felipe Monteirof16b6b32018-06-28 19:32:59 -0400217 except Exception as actual_exception:
Mykola Yakovliev11376ab2018-08-06 15:34:22 -0500218 caught_exception = actual_exception
219
Felipe Monteirof16b6b32018-06-28 19:32:59 -0400220 if _check_for_expected_mismatch_exception(expected_exception,
221 actual_exception):
222 LOG.error('Expected and actual exceptions do not match. '
223 'Expected: %s. Actual: %s.',
224 expected_exception,
225 actual_exception.__class__)
226 raise rbac_exceptions.RbacExpectedWrongException(
227 expected=expected_exception,
228 actual=actual_exception.__class__,
229 exception=actual_exception)
230 else:
231 with excutils.save_and_reraise_exception():
232 exc_info = sys.exc_info()
233 error_details = six.text_type(exc_info[1])
234 msg = ("An unexpected exception has occurred during "
235 "test: %s. Exception was: %s" % (
236 test_func.__name__, error_details))
237 test_status = 'Error, %s' % (error_details)
238 LOG.error(msg)
DavidPurcell029d8c32017-01-06 15:27:41 -0500239 else:
240 if not allowed:
Felipe Monteiro44d77842018-03-21 02:42:59 +0000241 msg = (
242 "OverPermission: Role %s was allowed to perform the "
243 "following disallowed actions: %s" % (
244 role, sorted(disallowed_rules)
245 )
246 )
247 LOG.error(msg)
Felipe Monteirof16b6b32018-06-28 19:32:59 -0400248 raise rbac_exceptions.RbacOverPermissionException(msg)
raiesmh088590c0c2017-03-14 18:06:52 +0530249 finally:
Sean Pryor7f8993f2017-08-14 12:53:17 -0400250 if CONF.patrole_log.enable_reporting:
251 RBACLOG.info(
Felipe Monteiroc0cb7eb2018-06-19 19:50:36 -0400252 "[Service]: %s, [Test]: %s, [Rules]: %s, "
Sean Pryor7f8993f2017-08-14 12:53:17 -0400253 "[Expected]: %s, [Actual]: %s",
Felipe Monteiroc0cb7eb2018-06-19 19:50:36 -0400254 service, test_func.__name__, ', '.join(rules),
Sean Pryor7f8993f2017-08-14 12:53:17 -0400255 "Allowed" if allowed else "Denied",
256 test_status)
Felipe Monteiro78fc4892017-04-12 21:33:39 +0100257
Mykola Yakovliev11376ab2018-08-06 15:34:22 -0500258 # Sanity-check that ``override_role`` was called to eliminate
259 # false-positives and bad test flows resulting from exceptions
260 # getting raised too early, too late or not at all, within
261 # the scope of an RBAC test.
262 _validate_override_role_called(
263 test_obj,
264 actual_exception=caught_exception)
265
Felipe Monteiro2fe986d2018-03-20 21:53:51 +0000266 return wrapper
DavidPurcell029d8c32017-01-06 15:27:41 -0500267 return decorator
Rick Bartra12998942017-03-17 17:35:45 -0400268
269
Cliff Parsons35a77112018-05-07 14:03:40 -0500270def _prepare_multi_policy(rule, rules, exp_error_code, exp_error_codes):
Cliff Parsons35a77112018-05-07 14:03:40 -0500271 if exp_error_codes:
272 if not rules:
273 msg = ("The `rules` list must be provided if using the "
274 "`expected_error_codes` list.")
275 raise ValueError(msg)
276 if len(rules) != len(exp_error_codes):
277 msg = ("The `expected_error_codes` list is not the same length "
278 "as the `rules` list.")
279 raise ValueError(msg)
280 if exp_error_code:
281 deprecation_msg = (
282 "The `exp_error_code` argument has been deprecated in favor "
283 "of `exp_error_codes` and will be removed in a future "
284 "version.")
285 versionutils.report_deprecated_feature(LOG, deprecation_msg)
286 LOG.debug("The `exp_error_codes` argument will be used instead of "
287 "`exp_error_code`.")
288 if not isinstance(exp_error_codes, (tuple, list)):
289 exp_error_codes = [exp_error_codes]
290 else:
291 exp_error_codes = []
292 if exp_error_code:
293 exp_error_codes.append(exp_error_code)
294
Felipe Monteiro44d77842018-03-21 02:42:59 +0000295 if rules is None:
296 rules = []
297 elif not isinstance(rules, (tuple, list)):
298 rules = [rules]
299 if rule:
300 deprecation_msg = (
301 "The `rule` argument has been deprecated in favor of `rules` "
302 "and will be removed in a future version.")
303 versionutils.report_deprecated_feature(LOG, deprecation_msg)
304 if rules:
305 LOG.debug("The `rules` argument will be used instead of `rule`.")
306 else:
307 rules.append(rule)
Cliff Parsons35a77112018-05-07 14:03:40 -0500308
309 # Fill in the exp_error_codes if needed. This is needed for the scenarios
310 # where no exp_error_codes array is provided, so the error codes must be
311 # set to the default error code value and there must be the same number
312 # of error codes as rules.
313 num_ecs = len(exp_error_codes)
314 num_rules = len(rules)
315 if (num_ecs < num_rules):
316 for i in range(num_rules - num_ecs):
317 exp_error_codes.append(_DEFAULT_ERROR_CODE)
318
319 return rules, exp_error_codes
Felipe Monteiro44d77842018-03-21 02:42:59 +0000320
321
Felipe Monteiro318a0bf2018-02-27 06:57:10 -0500322def _is_authorized(test_obj, service, rule, extra_target_data):
Felipe Monteirodea13842017-07-05 04:11:18 +0100323 """Validates whether current RBAC role has permission to do policy action.
324
Felipe Monteirof2b58d72017-08-31 22:40:36 +0100325 :param test_obj: An instance or subclass of ``tempest.test.BaseTestCase``.
Felipe Monteiro01d633b2017-08-16 20:17:26 +0100326 :param service: The OpenStack service that enforces ``rule``.
327 :param rule: The name of the policy action. Examples include
328 "identity:create_user" or "os_compute_api:os-agents".
Felipe Monteirof2b58d72017-08-31 22:40:36 +0100329 :param extra_target_data: Dictionary, keyed with ``oslo.policy`` generic
Felipe Monteiro01d633b2017-08-16 20:17:26 +0100330 check names, whose values are string literals that reference nested
Felipe Monteirof2b58d72017-08-31 22:40:36 +0100331 ``tempest.test.BaseTestCase`` attributes. Used by ``oslo.policy`` for
Felipe Monteiro01d633b2017-08-16 20:17:26 +0100332 performing matching against attributes that are sent along with the API
333 calls.
Sean Pryor7f8993f2017-08-14 12:53:17 -0400334
Felipe Monteiro01d633b2017-08-16 20:17:26 +0100335 :returns: True if the current RBAC role can perform the policy action,
336 else False.
Sean Pryor7f8993f2017-08-14 12:53:17 -0400337
Felipe Monteiro01d633b2017-08-16 20:17:26 +0100338 :raises RbacResourceSetupFailed: If `project_id` or `user_id` are missing
339 from the `auth_provider` attribute in `test_obj`.
Felipe Monteirodea13842017-07-05 04:11:18 +0100340 """
Sean Pryor7f8993f2017-08-14 12:53:17 -0400341
Felipe Monteiro78fc4892017-04-12 21:33:39 +0100342 try:
Felipe Monteiroe8d93e02017-07-19 20:52:20 +0100343 project_id = test_obj.os_primary.credentials.project_id
344 user_id = test_obj.os_primary.credentials.user_id
Felipe Monteiro78fc4892017-04-12 21:33:39 +0100345 except AttributeError as e:
Felipe Monteiroe8d93e02017-07-19 20:52:20 +0100346 msg = ("{0}: project_id or user_id not found in os_primary.credentials"
347 .format(e))
Felipe Monteiro78fc4892017-04-12 21:33:39 +0100348 LOG.error(msg)
349 raise rbac_exceptions.RbacResourceSetupFailed(msg)
350
Felipe Monteiro4ef7e532018-03-11 07:17:11 -0400351 role = CONF.patrole.rbac_test_role
352 # 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)
359 authority = policy_authority.PolicyAuthority(
360 project_id, user_id, service,
361 extra_target_data=formatted_target_data)
362 is_allowed = authority.allowed(rule, role)
Felipe Monteiro78fc4892017-04-12 21:33:39 +0100363
Felipe Monteiro4ef7e532018-03-11 07:17:11 -0400364 if is_allowed:
Felipe Monteiroc0cb7eb2018-06-19 19:50:36 -0400365 LOG.debug("[Policy action]: %s, [Role]: %s is allowed!", rule,
Felipe Monteiro4ef7e532018-03-11 07:17:11 -0400366 role)
367 else:
Felipe Monteiroc0cb7eb2018-06-19 19:50:36 -0400368 LOG.debug("[Policy action]: %s, [Role]: %s is NOT allowed!",
Felipe Monteiro4ef7e532018-03-11 07:17:11 -0400369 rule, role)
370
371 return is_allowed
Felipe Monteiro78fc4892017-04-12 21:33:39 +0100372
373
Felipe Monteiroc0cb7eb2018-06-19 19:50:36 -0400374def _get_exception_type(expected_error_code=_DEFAULT_ERROR_CODE):
Felipe Monteiro973a1bc2017-06-14 21:23:54 +0100375 """Dynamically calculate the expected exception to be caught.
376
377 Dynamically calculate the expected exception to be caught by the test case.
Felipe Monteirof2b58d72017-08-31 22:40:36 +0100378 Only ``Forbidden`` and ``NotFound`` exceptions are permitted. ``NotFound``
379 is supported because Neutron, for security reasons, masks ``Forbidden``
380 exceptions as ``NotFound`` exceptions.
Felipe Monteiro973a1bc2017-06-14 21:23:54 +0100381
382 :param expected_error_code: the integer representation of the expected
Felipe Monteirof2b58d72017-08-31 22:40:36 +0100383 exception to be caught. Must be contained in
384 ``_SUPPORTED_ERROR_CODES``.
Felipe Monteiro973a1bc2017-06-14 21:23:54 +0100385 :returns: tuple of the exception type corresponding to
Felipe Monteirof2b58d72017-08-31 22:40:36 +0100386 ``expected_error_code`` and a message explaining that a non-Forbidden
Felipe Monteiro973a1bc2017-06-14 21:23:54 +0100387 exception was expected, if applicable.
388 """
Rick Bartra12998942017-03-17 17:35:45 -0400389 expected_exception = None
390 irregular_msg = None
Felipe Monteiro973a1bc2017-06-14 21:23:54 +0100391
392 if not isinstance(expected_error_code, six.integer_types) \
Sean Pryor7f8993f2017-08-14 12:53:17 -0400393 or expected_error_code not in _SUPPORTED_ERROR_CODES:
Felipe Monteiro973a1bc2017-06-14 21:23:54 +0100394 msg = ("Please pass an expected error code. Currently "
395 "supported codes: {0}".format(_SUPPORTED_ERROR_CODES))
396 LOG.error(msg)
397 raise rbac_exceptions.RbacInvalidErrorCode(msg)
Felipe Monteiro78fc4892017-04-12 21:33:39 +0100398
Rick Bartra12998942017-03-17 17:35:45 -0400399 if expected_error_code == 403:
Felipe Monteiro51299a12018-06-28 20:03:27 -0400400 expected_exception = lib_exc.Forbidden
Rick Bartra12998942017-03-17 17:35:45 -0400401 elif expected_error_code == 404:
Felipe Monteiro51299a12018-06-28 20:03:27 -0400402 expected_exception = lib_exc.NotFound
Felipe Monteiroc0cb7eb2018-06-19 19:50:36 -0400403 irregular_msg = ("NotFound exception was caught for test %s. Expected "
404 "policies which may have caused the error: %s. The "
405 "service %s throws a 404 instead of a 403, which is "
Mykola Yakovliev11376ab2018-08-06 15:34:22 -0500406 "irregular")
Rick Bartra12998942017-03-17 17:35:45 -0400407 return expected_exception, irregular_msg
Felipe Monteirofd1db982017-04-13 21:19:41 +0100408
409
410def _format_extra_target_data(test_obj, extra_target_data):
411 """Formats the "extra_target_data" dictionary with correct test data.
412
413 Before being formatted, "extra_target_data" is a dictionary that maps a
Felipe Monteiro01d633b2017-08-16 20:17:26 +0100414 policy string like "trust.trustor_user_id" to a nested list of
Felipe Monteirof2b58d72017-08-31 22:40:36 +0100415 ``tempest.test.BaseTestCase`` attributes. For example, the attribute list
Masayuki Igawa80b9aab2018-01-09 17:00:45 +0900416 in::
Felipe Monteirofd1db982017-04-13 21:19:41 +0100417
Masayuki Igawa80b9aab2018-01-09 17:00:45 +0900418 "trust.trustor_user_id": "os.auth_provider.credentials.user_id"
Felipe Monteirofd1db982017-04-13 21:19:41 +0100419
Felipe Monteiro01d633b2017-08-16 20:17:26 +0100420 is parsed by iteratively calling ``getattr`` until the value of "user_id"
Masayuki Igawa80b9aab2018-01-09 17:00:45 +0900421 is resolved. The resulting dictionary returns::
Felipe Monteirofd1db982017-04-13 21:19:41 +0100422
Masayuki Igawa80b9aab2018-01-09 17:00:45 +0900423 "trust.trustor_user_id": "the user_id of the `os_primary` credential"
Felipe Monteirofd1db982017-04-13 21:19:41 +0100424
Felipe Monteirof2b58d72017-08-31 22:40:36 +0100425 :param test_obj: An instance or subclass of ``tempest.test.BaseTestCase``.
426 :param extra_target_data: Dictionary, keyed with ``oslo.policy`` generic
Felipe Monteiro01d633b2017-08-16 20:17:26 +0100427 check names, whose values are string literals that reference nested
Felipe Monteirof2b58d72017-08-31 22:40:36 +0100428 ``tempest.test.BaseTestCase`` attributes. Used by ``oslo.policy`` for
Felipe Monteiro01d633b2017-08-16 20:17:26 +0100429 performing matching against attributes that are sent along with the API
430 calls.
431 :returns: Dictionary containing additional object data needed by
Felipe Monteirof2b58d72017-08-31 22:40:36 +0100432 ``oslo.policy`` to validate generic checks.
Felipe Monteirofd1db982017-04-13 21:19:41 +0100433 """
434 attr_value = test_obj
435 formatted_target_data = {}
436
437 for user_attribute, attr_string in extra_target_data.items():
438 attrs = attr_string.split('.')
439 for attr in attrs:
440 attr_value = getattr(attr_value, attr)
441 formatted_target_data[user_attribute] = attr_value
442
443 return formatted_target_data
Felipe Monteirof16b6b32018-06-28 19:32:59 -0400444
445
446def _check_for_expected_mismatch_exception(expected_exception,
447 actual_exception):
Mykola Yakovliev11376ab2018-08-06 15:34:22 -0500448 """Checks that ``expected_exception`` matches ``actual_exception``.
449
450 Since Patrole must handle 403/404 it is important that the expected and
451 actual error codes match.
452
453 :param excepted_exception: Expected exception for test.
454 :param actual_exception: Actual exception raised by test.
455 :returns: True if match, else False.
456 :rtype: boolean
457 """
Felipe Monteiro51299a12018-06-28 20:03:27 -0400458 permission_exceptions = (lib_exc.Forbidden, lib_exc.NotFound)
Felipe Monteirof16b6b32018-06-28 19:32:59 -0400459 if isinstance(actual_exception, permission_exceptions):
460 if not isinstance(actual_exception, expected_exception.__class__):
461 return True
462 return False
Mykola Yakovliev11376ab2018-08-06 15:34:22 -0500463
464
465def _validate_override_role_called(test_obj, actual_exception):
466 """Validates that :func:`rbac_utils.RbacUtils.override_role` is called
467 during each Patrole test.
468
469 Useful for validating that the expected exception isn't raised too early
470 (before ``override_role`` call) or too late (after ``override_call``) or
471 at all (which is a bad test).
472
473 :param test_obj: An instance or subclass of ``tempest.test.BaseTestCase``.
474 :param actual_exception: Actual exception raised by test.
475 :raises RbacOverrideRoleException: If ``override_role`` isn't called, is
476 called too early, or is called too late.
477 """
478 called = test_obj._validate_override_role_called()
479 base_msg = ('This error is unrelated to RBAC and is due to either '
480 'an API or override role failure. Exception: %s' %
481 actual_exception)
482
483 if not called:
484 if actual_exception is not None:
485 msg = ('Caught exception (%s) but it was raised before the '
486 '`override_role` context. ' % actual_exception.__class__)
487 else:
488 msg = 'Test missing required `override_role` call. '
489 msg += base_msg
490 LOG.error(msg)
491 raise rbac_exceptions.RbacOverrideRoleException(msg)
492 else:
493 exc_caught_in_ctx = test_obj._validate_override_role_caught_exc()
494 # This block is only executed if ``override_role`` is called. If
495 # an exception is raised and the exception wasn't raised in the
496 # ``override_role`` context and if the exception isn't a valid
497 # exception type (instance of ``BasePatroleException``), then this is
498 # a legitimate error.
499 if (not exc_caught_in_ctx and
500 actual_exception is not None and
501 not isinstance(actual_exception,
502 rbac_exceptions.BasePatroleException)):
503 msg = ('Caught exception (%s) but it was raised after the '
504 '`override_role` context. ' % actual_exception.__class__)
505 msg += base_msg
506 LOG.error(msg)
507 raise rbac_exceptions.RbacOverrideRoleException(msg)