blob: d3b057cd8db2216ab5a0bf8825f41879ca21f3c0 [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
Chi Lo8c04bd82018-06-01 16:21:50 -050041def action(service,
42 rule='',
43 rules=None,
44 expected_error_code=_DEFAULT_ERROR_CODE,
45 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
53 * the test role defined by ``[patrole] rbac_test_role``
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".
Chi Lo8c04bd82018-06-01 16:21:50 -050078 :param rule: (DEPRECATED) A policy action defined in a policy.json file
79 or in code. Also accepts a callable that returns a policy action.
80 :type rule: str or callable
81 :param rules: A list of policy actions defined in a policy.json file
Felipe Monteiro44d77842018-03-21 02:42:59 +000082 or in code. The rules are logical-ANDed together to derive the expected
Chi Lo8c04bd82018-06-01 16:21:50 -050083 result. Also accepts list of callables that return a policy action.
Felipe Monteiro01d633b2017-08-16 20:17:26 +010084
85 .. note::
86
87 Patrole currently only supports custom JSON policy files.
88
Chi Lo8c04bd82018-06-01 16:21:50 -050089 :type rules: list[str] or list[callable]
Felipe Monteiro318fa3b2018-06-19 16:53:33 -040090 :param int expected_error_code: (DEPRECATED) Overrides default value of 403
91 (Forbidden) with endpoint-specific error code. Currently only supports
92 403 and 404. Support for 404 is needed because some services, like
93 Neutron, intentionally throw a 404 for security reasons.
Felipe Monteirod5d76b82017-03-20 23:18:50 +000094
Felipe Monteiro01d633b2017-08-16 20:17:26 +010095 .. warning::
96
97 A 404 should not be provided *unless* the endpoint masks a
Felipe Monteirof2b58d72017-08-31 22:40:36 +010098 ``Forbidden`` exception as a ``NotFound`` exception.
Felipe Monteiro01d633b2017-08-16 20:17:26 +010099
Cliff Parsons35a77112018-05-07 14:03:40 -0500100 :param list expected_error_codes: When the ``rules`` list parameter is
101 used, then this list indicates the expected error code to use if one
102 of the rules does not allow the role being tested. This list must
103 coincide with and its elements remain in the same order as the rules
104 in the rules list.
105
106 Example::
Felipe Monteiro318fa3b2018-06-19 16:53:33 -0400107
Cliff Parsons35a77112018-05-07 14:03:40 -0500108 rules=["api_action1", "api_action2"]
109 expected_error_codes=[404, 403]
110
111 a) If api_action1 fails and api_action2 passes, then the expected
112 error code is 404.
113 b) if api_action2 fails and api_action1 passes, then the expected
114 error code is 403.
115 c) if both api_action1 and api_action2 fail, then the expected error
116 code is the first error seen (404).
117
ghanshyam98437d42018-08-17 08:51:43 +0000118 If it is not passed, then it is defaulted to 403.
Cliff Parsons35a77112018-05-07 14:03:40 -0500119
Felipe Monteiro44d77842018-03-21 02:42:59 +0000120 :param dict extra_target_data: Dictionary, keyed with ``oslo.policy``
121 generic check names, whose values are string literals that reference
122 nested ``tempest.test.BaseTestCase`` attributes. Used by
123 ``oslo.policy`` for performing matching against attributes that are
124 sent along with the API calls. Example::
Felipe Monteiro01d633b2017-08-16 20:17:26 +0100125
126 extra_target_data={
127 "target.token.user_id":
128 "os_alt.auth_provider.credentials.user_id"
129 })
130
Felipe Monteiro51299a12018-06-28 20:03:27 -0400131 :raises RbacInvalidServiceException: If ``service`` is invalid.
Felipe Monteirof16b6b32018-06-28 19:32:59 -0400132 :raises RbacUnderPermissionException: For item (2) above.
133 :raises RbacOverPermissionException: For item (3) above.
134 :raises RbacExpectedWrongException: When a 403 is expected but a 404
135 is raised instead or vice versa.
Felipe Monteiro01d633b2017-08-16 20:17:26 +0100136
137 Examples::
138
139 @rbac_rule_validation.action(
140 service="nova", rule="os_compute_api:os-agents")
141 def test_list_agents_rbac(self):
Felipe Monteiro1c8620a2018-02-25 18:52:22 +0000142 # The call to `override_role` is mandatory.
143 with self.rbac_utils.override_role(self):
144 self.agents_client.list_agents()
Felipe Monteirod5d76b82017-03-20 23:18:50 +0000145 """
Felipe Monteiro0854ded2017-05-05 16:30:55 +0100146
147 if extra_target_data is None:
148 extra_target_data = {}
149
Cliff Parsons35a77112018-05-07 14:03:40 -0500150 rules, expected_error_codes = _prepare_multi_policy(rule, rules,
151 expected_error_code,
152 expected_error_codes)
Felipe Monteiro44d77842018-03-21 02:42:59 +0000153
Sean Pryor7f8993f2017-08-14 12:53:17 -0400154 def decorator(test_func):
Felipe Monteirof6eb8622017-08-06 06:08:02 +0100155 role = CONF.patrole.rbac_test_role
Felipe Monteiro78fc4892017-04-12 21:33:39 +0100156
Felipe Monteiro2fe986d2018-03-20 21:53:51 +0000157 @functools.wraps(test_func)
DavidPurcell029d8c32017-01-06 15:27:41 -0500158 def wrapper(*args, **kwargs):
Felipe Monteiro78fc4892017-04-12 21:33:39 +0100159 if args and isinstance(args[0], test.BaseTestCase):
160 test_obj = args[0]
161 else:
162 raise rbac_exceptions.RbacResourceSetupFailed(
163 '`rbac_rule_validation` decorator can only be applied to '
164 'an instance of `tempest.test.BaseTestCase`.')
raiesmh088590c0c2017-03-14 18:06:52 +0530165
Felipe Monteiro44d77842018-03-21 02:42:59 +0000166 allowed = True
167 disallowed_rules = []
168 for rule in rules:
169 _allowed = _is_authorized(
170 test_obj, service, rule, extra_target_data)
171 if not _allowed:
172 disallowed_rules.append(rule)
173 allowed = allowed and _allowed
Felipe Monteirod5d76b82017-03-20 23:18:50 +0000174
Cliff Parsons35a77112018-05-07 14:03:40 -0500175 exp_error_code = expected_error_code
176 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])
184
Rick Bartra12998942017-03-17 17:35:45 -0400185 expected_exception, irregular_msg = _get_exception_type(
Cliff Parsons35a77112018-05-07 14:03:40 -0500186 exp_error_code)
DavidPurcell029d8c32017-01-06 15:27:41 -0500187
Mykola Yakovliev11376ab2018-08-06 15:34:22 -0500188 caught_exception = None
Sean Pryor7f8993f2017-08-14 12:53:17 -0400189 test_status = 'Allowed'
190
DavidPurcell029d8c32017-01-06 15:27:41 -0500191 try:
Sean Pryor7f8993f2017-08-14 12:53:17 -0400192 test_func(*args, **kwargs)
Felipe Monteiro51299a12018-06-28 20:03:27 -0400193 except rbac_exceptions.RbacInvalidServiceException:
194 with excutils.save_and_reraise_exception():
195 msg = ("%s is not a valid service." % service)
196 # FIXME(felipemonteiro): This test_status is logged too
197 # late. Need a function to log it before re-raising.
198 test_status = ('Error, %s' % (msg))
199 LOG.error(msg)
Samantha Blanco36bea052017-07-19 12:01:59 -0400200 except (expected_exception,
201 rbac_exceptions.RbacConflictingPolicies,
Mykola Yakovliev11376ab2018-08-06 15:34:22 -0500202 rbac_exceptions.RbacMalformedResponse) as actual_exception:
203 caught_exception = actual_exception
Sean Pryor7f8993f2017-08-14 12:53:17 -0400204 test_status = 'Denied'
Mykola Yakovliev11376ab2018-08-06 15:34:22 -0500205
Felipe Monteiro8eda8cc2017-03-22 14:15:14 +0000206 if irregular_msg:
Felipe Monteiroc0cb7eb2018-06-19 19:50:36 -0400207 LOG.warning(irregular_msg,
208 test_func.__name__,
209 ', '.join(rules),
210 service)
Mykola Yakovliev11376ab2018-08-06 15:34:22 -0500211
DavidPurcell029d8c32017-01-06 15:27:41 -0500212 if allowed:
Felipe Monteiro44d77842018-03-21 02:42:59 +0000213 msg = ("Role %s was not allowed to perform the following "
214 "actions: %s. Expected allowed actions: %s. "
215 "Expected disallowed actions: %s." % (
216 role, sorted(rules),
217 sorted(set(rules) - set(disallowed_rules)),
218 sorted(disallowed_rules)))
DavidPurcell029d8c32017-01-06 15:27:41 -0500219 LOG.error(msg)
Felipe Monteirof16b6b32018-06-28 19:32:59 -0400220 raise rbac_exceptions.RbacUnderPermissionException(
Mykola Yakovliev11376ab2018-08-06 15:34:22 -0500221 "%s Exception was: %s" % (msg, actual_exception))
Felipe Monteirof16b6b32018-06-28 19:32:59 -0400222 except Exception as actual_exception:
Mykola Yakovliev11376ab2018-08-06 15:34:22 -0500223 caught_exception = actual_exception
224
Felipe Monteirof16b6b32018-06-28 19:32:59 -0400225 if _check_for_expected_mismatch_exception(expected_exception,
226 actual_exception):
227 LOG.error('Expected and actual exceptions do not match. '
228 'Expected: %s. Actual: %s.',
229 expected_exception,
230 actual_exception.__class__)
231 raise rbac_exceptions.RbacExpectedWrongException(
232 expected=expected_exception,
233 actual=actual_exception.__class__,
234 exception=actual_exception)
235 else:
236 with excutils.save_and_reraise_exception():
237 exc_info = sys.exc_info()
238 error_details = six.text_type(exc_info[1])
239 msg = ("An unexpected exception has occurred during "
240 "test: %s. Exception was: %s" % (
241 test_func.__name__, error_details))
242 test_status = 'Error, %s' % (error_details)
243 LOG.error(msg)
DavidPurcell029d8c32017-01-06 15:27:41 -0500244 else:
245 if not allowed:
Felipe Monteiro44d77842018-03-21 02:42:59 +0000246 msg = (
247 "OverPermission: Role %s was allowed to perform the "
248 "following disallowed actions: %s" % (
249 role, sorted(disallowed_rules)
250 )
251 )
252 LOG.error(msg)
Felipe Monteirof16b6b32018-06-28 19:32:59 -0400253 raise rbac_exceptions.RbacOverPermissionException(msg)
raiesmh088590c0c2017-03-14 18:06:52 +0530254 finally:
Sean Pryor7f8993f2017-08-14 12:53:17 -0400255 if CONF.patrole_log.enable_reporting:
256 RBACLOG.info(
Felipe Monteiroc0cb7eb2018-06-19 19:50:36 -0400257 "[Service]: %s, [Test]: %s, [Rules]: %s, "
Sean Pryor7f8993f2017-08-14 12:53:17 -0400258 "[Expected]: %s, [Actual]: %s",
Felipe Monteiroc0cb7eb2018-06-19 19:50:36 -0400259 service, test_func.__name__, ', '.join(rules),
Sean Pryor7f8993f2017-08-14 12:53:17 -0400260 "Allowed" if allowed else "Denied",
261 test_status)
Felipe Monteiro78fc4892017-04-12 21:33:39 +0100262
Mykola Yakovliev11376ab2018-08-06 15:34:22 -0500263 # Sanity-check that ``override_role`` was called to eliminate
264 # false-positives and bad test flows resulting from exceptions
265 # getting raised too early, too late or not at all, within
266 # the scope of an RBAC test.
267 _validate_override_role_called(
268 test_obj,
269 actual_exception=caught_exception)
270
Felipe Monteiro2fe986d2018-03-20 21:53:51 +0000271 return wrapper
DavidPurcell029d8c32017-01-06 15:27:41 -0500272 return decorator
Rick Bartra12998942017-03-17 17:35:45 -0400273
274
Cliff Parsons35a77112018-05-07 14:03:40 -0500275def _prepare_multi_policy(rule, rules, exp_error_code, exp_error_codes):
Cliff Parsons35a77112018-05-07 14:03:40 -0500276 if exp_error_codes:
277 if not rules:
278 msg = ("The `rules` list must be provided if using the "
279 "`expected_error_codes` list.")
280 raise ValueError(msg)
281 if len(rules) != len(exp_error_codes):
282 msg = ("The `expected_error_codes` list is not the same length "
283 "as the `rules` list.")
284 raise ValueError(msg)
285 if exp_error_code:
286 deprecation_msg = (
287 "The `exp_error_code` argument has been deprecated in favor "
288 "of `exp_error_codes` and will be removed in a future "
289 "version.")
290 versionutils.report_deprecated_feature(LOG, deprecation_msg)
291 LOG.debug("The `exp_error_codes` argument will be used instead of "
292 "`exp_error_code`.")
293 if not isinstance(exp_error_codes, (tuple, list)):
294 exp_error_codes = [exp_error_codes]
295 else:
296 exp_error_codes = []
297 if exp_error_code:
298 exp_error_codes.append(exp_error_code)
299
Felipe Monteiro44d77842018-03-21 02:42:59 +0000300 if rules is None:
301 rules = []
302 elif not isinstance(rules, (tuple, list)):
303 rules = [rules]
304 if rule:
305 deprecation_msg = (
306 "The `rule` argument has been deprecated in favor of `rules` "
307 "and will be removed in a future version.")
308 versionutils.report_deprecated_feature(LOG, deprecation_msg)
309 if rules:
310 LOG.debug("The `rules` argument will be used instead of `rule`.")
311 else:
312 rules.append(rule)
Cliff Parsons35a77112018-05-07 14:03:40 -0500313
314 # Fill in the exp_error_codes if needed. This is needed for the scenarios
315 # where no exp_error_codes array is provided, so the error codes must be
316 # set to the default error code value and there must be the same number
317 # of error codes as rules.
318 num_ecs = len(exp_error_codes)
319 num_rules = len(rules)
320 if (num_ecs < num_rules):
321 for i in range(num_rules - num_ecs):
322 exp_error_codes.append(_DEFAULT_ERROR_CODE)
323
Chi Lo8c04bd82018-06-01 16:21:50 -0500324 evaluated_rules = [
325 r() if callable(r) else r for r in rules
326 ]
327
328 return evaluated_rules, exp_error_codes
Felipe Monteiro44d77842018-03-21 02:42:59 +0000329
330
Felipe Monteiro318a0bf2018-02-27 06:57:10 -0500331def _is_authorized(test_obj, service, rule, extra_target_data):
Felipe Monteirodea13842017-07-05 04:11:18 +0100332 """Validates whether current RBAC role has permission to do policy action.
333
Felipe Monteirof2b58d72017-08-31 22:40:36 +0100334 :param test_obj: An instance or subclass of ``tempest.test.BaseTestCase``.
Felipe Monteiro01d633b2017-08-16 20:17:26 +0100335 :param service: The OpenStack service that enforces ``rule``.
336 :param rule: The name of the policy action. Examples include
337 "identity:create_user" or "os_compute_api:os-agents".
Felipe Monteirof2b58d72017-08-31 22:40:36 +0100338 :param extra_target_data: Dictionary, keyed with ``oslo.policy`` generic
Felipe Monteiro01d633b2017-08-16 20:17:26 +0100339 check names, whose values are string literals that reference nested
Felipe Monteirof2b58d72017-08-31 22:40:36 +0100340 ``tempest.test.BaseTestCase`` attributes. Used by ``oslo.policy`` for
Felipe Monteiro01d633b2017-08-16 20:17:26 +0100341 performing matching against attributes that are sent along with the API
342 calls.
Sean Pryor7f8993f2017-08-14 12:53:17 -0400343
Felipe Monteiro01d633b2017-08-16 20:17:26 +0100344 :returns: True if the current RBAC role can perform the policy action,
345 else False.
Sean Pryor7f8993f2017-08-14 12:53:17 -0400346
Felipe Monteiro01d633b2017-08-16 20:17:26 +0100347 :raises RbacResourceSetupFailed: If `project_id` or `user_id` are missing
348 from the `auth_provider` attribute in `test_obj`.
Felipe Monteirodea13842017-07-05 04:11:18 +0100349 """
Sean Pryor7f8993f2017-08-14 12:53:17 -0400350
Felipe Monteiro78fc4892017-04-12 21:33:39 +0100351 try:
Felipe Monteiroe8d93e02017-07-19 20:52:20 +0100352 project_id = test_obj.os_primary.credentials.project_id
353 user_id = test_obj.os_primary.credentials.user_id
Felipe Monteiro78fc4892017-04-12 21:33:39 +0100354 except AttributeError as e:
Felipe Monteiroe8d93e02017-07-19 20:52:20 +0100355 msg = ("{0}: project_id or user_id not found in os_primary.credentials"
356 .format(e))
Felipe Monteiro78fc4892017-04-12 21:33:39 +0100357 LOG.error(msg)
358 raise rbac_exceptions.RbacResourceSetupFailed(msg)
359
Felipe Monteiro4ef7e532018-03-11 07:17:11 -0400360 role = CONF.patrole.rbac_test_role
361 # Test RBAC against custom requirements. Otherwise use oslo.policy.
362 if CONF.patrole.test_custom_requirements:
363 authority = requirements_authority.RequirementsAuthority(
364 CONF.patrole.custom_requirements_file, service)
365 else:
366 formatted_target_data = _format_extra_target_data(
367 test_obj, extra_target_data)
368 authority = policy_authority.PolicyAuthority(
369 project_id, user_id, service,
370 extra_target_data=formatted_target_data)
371 is_allowed = authority.allowed(rule, role)
Felipe Monteiro78fc4892017-04-12 21:33:39 +0100372
Felipe Monteiro4ef7e532018-03-11 07:17:11 -0400373 if is_allowed:
Felipe Monteiroc0cb7eb2018-06-19 19:50:36 -0400374 LOG.debug("[Policy action]: %s, [Role]: %s is allowed!", rule,
Felipe Monteiro4ef7e532018-03-11 07:17:11 -0400375 role)
376 else:
Felipe Monteiroc0cb7eb2018-06-19 19:50:36 -0400377 LOG.debug("[Policy action]: %s, [Role]: %s is NOT allowed!",
Felipe Monteiro4ef7e532018-03-11 07:17:11 -0400378 rule, role)
379
380 return is_allowed
Felipe Monteiro78fc4892017-04-12 21:33:39 +0100381
382
Felipe Monteiroc0cb7eb2018-06-19 19:50:36 -0400383def _get_exception_type(expected_error_code=_DEFAULT_ERROR_CODE):
Felipe Monteiro973a1bc2017-06-14 21:23:54 +0100384 """Dynamically calculate the expected exception to be caught.
385
386 Dynamically calculate the expected exception to be caught by the test case.
Felipe Monteirof2b58d72017-08-31 22:40:36 +0100387 Only ``Forbidden`` and ``NotFound`` exceptions are permitted. ``NotFound``
388 is supported because Neutron, for security reasons, masks ``Forbidden``
389 exceptions as ``NotFound`` exceptions.
Felipe Monteiro973a1bc2017-06-14 21:23:54 +0100390
391 :param expected_error_code: the integer representation of the expected
Felipe Monteirof2b58d72017-08-31 22:40:36 +0100392 exception to be caught. Must be contained in
393 ``_SUPPORTED_ERROR_CODES``.
Felipe Monteiro973a1bc2017-06-14 21:23:54 +0100394 :returns: tuple of the exception type corresponding to
Felipe Monteirof2b58d72017-08-31 22:40:36 +0100395 ``expected_error_code`` and a message explaining that a non-Forbidden
Felipe Monteiro973a1bc2017-06-14 21:23:54 +0100396 exception was expected, if applicable.
397 """
Rick Bartra12998942017-03-17 17:35:45 -0400398 expected_exception = None
399 irregular_msg = None
Felipe Monteiro973a1bc2017-06-14 21:23:54 +0100400
401 if not isinstance(expected_error_code, six.integer_types) \
Sean Pryor7f8993f2017-08-14 12:53:17 -0400402 or expected_error_code not in _SUPPORTED_ERROR_CODES:
Felipe Monteiro973a1bc2017-06-14 21:23:54 +0100403 msg = ("Please pass an expected error code. Currently "
404 "supported codes: {0}".format(_SUPPORTED_ERROR_CODES))
405 LOG.error(msg)
406 raise rbac_exceptions.RbacInvalidErrorCode(msg)
Felipe Monteiro78fc4892017-04-12 21:33:39 +0100407
Rick Bartra12998942017-03-17 17:35:45 -0400408 if expected_error_code == 403:
Felipe Monteiro51299a12018-06-28 20:03:27 -0400409 expected_exception = lib_exc.Forbidden
Rick Bartra12998942017-03-17 17:35:45 -0400410 elif expected_error_code == 404:
Felipe Monteiro51299a12018-06-28 20:03:27 -0400411 expected_exception = lib_exc.NotFound
Felipe Monteiroc0cb7eb2018-06-19 19:50:36 -0400412 irregular_msg = ("NotFound exception was caught for test %s. Expected "
413 "policies which may have caused the error: %s. The "
414 "service %s throws a 404 instead of a 403, which is "
Mykola Yakovliev11376ab2018-08-06 15:34:22 -0500415 "irregular")
Rick Bartra12998942017-03-17 17:35:45 -0400416 return expected_exception, irregular_msg
Felipe Monteirofd1db982017-04-13 21:19:41 +0100417
418
419def _format_extra_target_data(test_obj, extra_target_data):
420 """Formats the "extra_target_data" dictionary with correct test data.
421
422 Before being formatted, "extra_target_data" is a dictionary that maps a
Felipe Monteiro01d633b2017-08-16 20:17:26 +0100423 policy string like "trust.trustor_user_id" to a nested list of
Felipe Monteirof2b58d72017-08-31 22:40:36 +0100424 ``tempest.test.BaseTestCase`` attributes. For example, the attribute list
Masayuki Igawa80b9aab2018-01-09 17:00:45 +0900425 in::
Felipe Monteirofd1db982017-04-13 21:19:41 +0100426
Masayuki Igawa80b9aab2018-01-09 17:00:45 +0900427 "trust.trustor_user_id": "os.auth_provider.credentials.user_id"
Felipe Monteirofd1db982017-04-13 21:19:41 +0100428
Felipe Monteiro01d633b2017-08-16 20:17:26 +0100429 is parsed by iteratively calling ``getattr`` until the value of "user_id"
Masayuki Igawa80b9aab2018-01-09 17:00:45 +0900430 is resolved. The resulting dictionary returns::
Felipe Monteirofd1db982017-04-13 21:19:41 +0100431
Masayuki Igawa80b9aab2018-01-09 17:00:45 +0900432 "trust.trustor_user_id": "the user_id of the `os_primary` credential"
Felipe Monteirofd1db982017-04-13 21:19:41 +0100433
Felipe Monteirof2b58d72017-08-31 22:40:36 +0100434 :param test_obj: An instance or subclass of ``tempest.test.BaseTestCase``.
435 :param extra_target_data: Dictionary, keyed with ``oslo.policy`` generic
Felipe Monteiro01d633b2017-08-16 20:17:26 +0100436 check names, whose values are string literals that reference nested
Felipe Monteirof2b58d72017-08-31 22:40:36 +0100437 ``tempest.test.BaseTestCase`` attributes. Used by ``oslo.policy`` for
Felipe Monteiro01d633b2017-08-16 20:17:26 +0100438 performing matching against attributes that are sent along with the API
439 calls.
440 :returns: Dictionary containing additional object data needed by
Felipe Monteirof2b58d72017-08-31 22:40:36 +0100441 ``oslo.policy`` to validate generic checks.
Felipe Monteirofd1db982017-04-13 21:19:41 +0100442 """
443 attr_value = test_obj
444 formatted_target_data = {}
445
446 for user_attribute, attr_string in extra_target_data.items():
447 attrs = attr_string.split('.')
448 for attr in attrs:
449 attr_value = getattr(attr_value, attr)
450 formatted_target_data[user_attribute] = attr_value
451
452 return formatted_target_data
Felipe Monteirof16b6b32018-06-28 19:32:59 -0400453
454
455def _check_for_expected_mismatch_exception(expected_exception,
456 actual_exception):
Mykola Yakovliev11376ab2018-08-06 15:34:22 -0500457 """Checks that ``expected_exception`` matches ``actual_exception``.
458
459 Since Patrole must handle 403/404 it is important that the expected and
460 actual error codes match.
461
462 :param excepted_exception: Expected exception for test.
463 :param actual_exception: Actual exception raised by test.
464 :returns: True if match, else False.
465 :rtype: boolean
466 """
Felipe Monteiro51299a12018-06-28 20:03:27 -0400467 permission_exceptions = (lib_exc.Forbidden, lib_exc.NotFound)
Felipe Monteirof16b6b32018-06-28 19:32:59 -0400468 if isinstance(actual_exception, permission_exceptions):
469 if not isinstance(actual_exception, expected_exception.__class__):
470 return True
471 return False
Mykola Yakovliev11376ab2018-08-06 15:34:22 -0500472
473
474def _validate_override_role_called(test_obj, actual_exception):
475 """Validates that :func:`rbac_utils.RbacUtils.override_role` is called
476 during each Patrole test.
477
478 Useful for validating that the expected exception isn't raised too early
479 (before ``override_role`` call) or too late (after ``override_call``) or
480 at all (which is a bad test).
481
482 :param test_obj: An instance or subclass of ``tempest.test.BaseTestCase``.
483 :param actual_exception: Actual exception raised by test.
484 :raises RbacOverrideRoleException: If ``override_role`` isn't called, is
485 called too early, or is called too late.
486 """
487 called = test_obj._validate_override_role_called()
488 base_msg = ('This error is unrelated to RBAC and is due to either '
489 'an API or override role failure. Exception: %s' %
490 actual_exception)
491
492 if not called:
493 if actual_exception is not None:
494 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)