| DavidPurcell | b25f93d | 2017-01-27 12:46:27 -0500 | [diff] [blame] | 1 | # Copyright 2017 AT&T Corporation. | 
| DavidPurcell | 029d8c3 | 2017-01-06 15:27:41 -0500 | [diff] [blame] | 2 | # 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 Monteiro | 2fe986d | 2018-03-20 21:53:51 +0000 | [diff] [blame] | 16 | import functools | 
| Felipe Monteiro | b059565 | 2017-01-23 16:51:58 -0500 | [diff] [blame] | 17 | import logging | 
| Felipe Monteiro | 8eda8cc | 2017-03-22 14:15:14 +0000 | [diff] [blame] | 18 | import sys | 
|  | 19 |  | 
| Felipe Monteiro | 44d7784 | 2018-03-21 02:42:59 +0000 | [diff] [blame] | 20 | from oslo_log import versionutils | 
| Felipe Monteiro | 38f344b | 2017-11-03 12:59:15 +0000 | [diff] [blame] | 21 | from oslo_utils import excutils | 
| Felipe Monteiro | 8eda8cc | 2017-03-22 14:15:14 +0000 | [diff] [blame] | 22 | import six | 
| Felipe Monteiro | b059565 | 2017-01-23 16:51:58 -0500 | [diff] [blame] | 23 |  | 
| DavidPurcell | 029d8c3 | 2017-01-06 15:27:41 -0500 | [diff] [blame] | 24 | from tempest import config | 
| Felipe Monteiro | 51299a1 | 2018-06-28 20:03:27 -0400 | [diff] [blame] | 25 | from tempest.lib import exceptions as lib_exc | 
| raiesmh08 | 8590c0c | 2017-03-14 18:06:52 +0530 | [diff] [blame] | 26 | from tempest import test | 
| DavidPurcell | 029d8c3 | 2017-01-06 15:27:41 -0500 | [diff] [blame] | 27 |  | 
| Felipe Monteiro | 88a5bab | 2017-08-31 04:00:32 +0100 | [diff] [blame] | 28 | from patrole_tempest_plugin import policy_authority | 
| DavidPurcell | 029d8c3 | 2017-01-06 15:27:41 -0500 | [diff] [blame] | 29 | from patrole_tempest_plugin import rbac_exceptions | 
| Rick Bartra | ed95005 | 2017-06-29 17:20:33 -0400 | [diff] [blame] | 30 | from patrole_tempest_plugin import requirements_authority | 
| DavidPurcell | 029d8c3 | 2017-01-06 15:27:41 -0500 | [diff] [blame] | 31 |  | 
|  | 32 | CONF = config.CONF | 
|  | 33 | LOG = logging.getLogger(__name__) | 
|  | 34 |  | 
| Felipe Monteiro | 973a1bc | 2017-06-14 21:23:54 +0100 | [diff] [blame] | 35 | _SUPPORTED_ERROR_CODES = [403, 404] | 
| Cliff Parsons | 35a7711 | 2018-05-07 14:03:40 -0500 | [diff] [blame] | 36 | _DEFAULT_ERROR_CODE = 403 | 
| Felipe Monteiro | 973a1bc | 2017-06-14 21:23:54 +0100 | [diff] [blame] | 37 |  | 
| Sean Pryor | 7f8993f | 2017-08-14 12:53:17 -0400 | [diff] [blame] | 38 | RBACLOG = logging.getLogger('rbac_reporting') | 
|  | 39 |  | 
| DavidPurcell | 029d8c3 | 2017-01-06 15:27:41 -0500 | [diff] [blame] | 40 |  | 
| Cliff Parsons | 35a7711 | 2018-05-07 14:03:40 -0500 | [diff] [blame] | 41 | def action(service, rule='', rules=None, | 
|  | 42 | expected_error_code=_DEFAULT_ERROR_CODE, expected_error_codes=None, | 
| Felipe Monteiro | 44d7784 | 2018-03-21 02:42:59 +0000 | [diff] [blame] | 43 | extra_target_data=None): | 
| Felipe Monteiro | f2b58d7 | 2017-08-31 22:40:36 +0100 | [diff] [blame] | 44 | """A decorator for verifying OpenStack policy enforcement. | 
| Felipe Monteiro | d5d76b8 | 2017-03-20 23:18:50 +0000 | [diff] [blame] | 45 |  | 
| Felipe Monteiro | 01d633b | 2017-08-16 20:17:26 +0100 | [diff] [blame] | 46 | A decorator which allows for positive and negative RBAC testing. Given: | 
| Rick Bartra | ed95005 | 2017-06-29 17:20:33 -0400 | [diff] [blame] | 47 |  | 
| Masayuki Igawa | 80b9aab | 2018-01-09 17:00:45 +0900 | [diff] [blame] | 48 | * an OpenStack service, | 
|  | 49 | * a policy action (``rule``) enforced by that service, and | 
|  | 50 | * the test role defined by ``[patrole] rbac_test_role`` | 
| Felipe Monteiro | d5d76b8 | 2017-03-20 23:18:50 +0000 | [diff] [blame] | 51 |  | 
| Felipe Monteiro | 01d633b | 2017-08-16 20:17:26 +0100 | [diff] [blame] | 52 | determines whether the test role has sufficient permissions to perform an | 
|  | 53 | API call that enforces the ``rule``. | 
| Felipe Monteiro | d5d76b8 | 2017-03-20 23:18:50 +0000 | [diff] [blame] | 54 |  | 
| Felipe Monteiro | 01d633b | 2017-08-16 20:17:26 +0100 | [diff] [blame] | 55 | This decorator should only be applied to an instance or subclass of | 
| Masayuki Igawa | 80b9aab | 2018-01-09 17:00:45 +0900 | [diff] [blame] | 56 | ``tempest.test.BaseTestCase``. | 
| Felipe Monteiro | 01d633b | 2017-08-16 20:17:26 +0100 | [diff] [blame] | 57 |  | 
|  | 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 Monteiro | f16b6b3 | 2018-06-28 19:32:59 -0400 | [diff] [blame] | 67 | ``RbacUnderPermissionException`` exception failure. | 
| Felipe Monteiro | 01d633b | 2017-08-16 20:17:26 +0100 | [diff] [blame] | 68 | 3) If *expected* is False and the test passes (*actual*), this results in | 
| Felipe Monteiro | f16b6b3 | 2018-06-28 19:32:59 -0400 | [diff] [blame] | 69 | an ``RbacOverPermissionException`` exception failure. | 
| Felipe Monteiro | 01d633b | 2017-08-16 20:17:26 +0100 | [diff] [blame] | 70 | 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 Monteiro | 44d7784 | 2018-03-21 02:42:59 +0000 | [diff] [blame] | 74 | :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 Monteiro | 01d633b | 2017-08-16 20:17:26 +0100 | [diff] [blame] | 80 |  | 
|  | 81 | .. note:: | 
|  | 82 |  | 
|  | 83 | Patrole currently only supports custom JSON policy files. | 
|  | 84 |  | 
| Felipe Monteiro | 318fa3b | 2018-06-19 16:53:33 -0400 | [diff] [blame] | 85 | :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 Monteiro | d5d76b8 | 2017-03-20 23:18:50 +0000 | [diff] [blame] | 89 |  | 
| Felipe Monteiro | 01d633b | 2017-08-16 20:17:26 +0100 | [diff] [blame] | 90 | .. warning:: | 
|  | 91 |  | 
|  | 92 | A 404 should not be provided *unless* the endpoint masks a | 
| Felipe Monteiro | f2b58d7 | 2017-08-31 22:40:36 +0100 | [diff] [blame] | 93 | ``Forbidden`` exception as a ``NotFound`` exception. | 
| Felipe Monteiro | 01d633b | 2017-08-16 20:17:26 +0100 | [diff] [blame] | 94 |  | 
| Cliff Parsons | 35a7711 | 2018-05-07 14:03:40 -0500 | [diff] [blame] | 95 | :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 Monteiro | 318fa3b | 2018-06-19 16:53:33 -0400 | [diff] [blame] | 102 |  | 
| Cliff Parsons | 35a7711 | 2018-05-07 14:03:40 -0500 | [diff] [blame] | 103 | 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 |  | 
| ghanshyam | 98437d4 | 2018-08-17 08:51:43 +0000 | [diff] [blame] | 113 | If it is not passed, then it is defaulted to 403. | 
| Cliff Parsons | 35a7711 | 2018-05-07 14:03:40 -0500 | [diff] [blame] | 114 |  | 
| Felipe Monteiro | 44d7784 | 2018-03-21 02:42:59 +0000 | [diff] [blame] | 115 | :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 Monteiro | 01d633b | 2017-08-16 20:17:26 +0100 | [diff] [blame] | 120 |  | 
|  | 121 | extra_target_data={ | 
|  | 122 | "target.token.user_id": | 
|  | 123 | "os_alt.auth_provider.credentials.user_id" | 
|  | 124 | }) | 
|  | 125 |  | 
| Felipe Monteiro | 51299a1 | 2018-06-28 20:03:27 -0400 | [diff] [blame] | 126 | :raises RbacInvalidServiceException: If ``service`` is invalid. | 
| Felipe Monteiro | f16b6b3 | 2018-06-28 19:32:59 -0400 | [diff] [blame] | 127 | :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 Monteiro | 01d633b | 2017-08-16 20:17:26 +0100 | [diff] [blame] | 131 |  | 
|  | 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 Monteiro | 1c8620a | 2018-02-25 18:52:22 +0000 | [diff] [blame] | 137 | # The call to `override_role` is mandatory. | 
|  | 138 | with self.rbac_utils.override_role(self): | 
|  | 139 | self.agents_client.list_agents() | 
| Felipe Monteiro | d5d76b8 | 2017-03-20 23:18:50 +0000 | [diff] [blame] | 140 | """ | 
| Felipe Monteiro | 0854ded | 2017-05-05 16:30:55 +0100 | [diff] [blame] | 141 |  | 
|  | 142 | if extra_target_data is None: | 
|  | 143 | extra_target_data = {} | 
|  | 144 |  | 
| Cliff Parsons | 35a7711 | 2018-05-07 14:03:40 -0500 | [diff] [blame] | 145 | rules, expected_error_codes = _prepare_multi_policy(rule, rules, | 
|  | 146 | expected_error_code, | 
|  | 147 | expected_error_codes) | 
| Felipe Monteiro | 44d7784 | 2018-03-21 02:42:59 +0000 | [diff] [blame] | 148 |  | 
| Sean Pryor | 7f8993f | 2017-08-14 12:53:17 -0400 | [diff] [blame] | 149 | def decorator(test_func): | 
| Felipe Monteiro | f6eb862 | 2017-08-06 06:08:02 +0100 | [diff] [blame] | 150 | role = CONF.patrole.rbac_test_role | 
| Felipe Monteiro | 78fc489 | 2017-04-12 21:33:39 +0100 | [diff] [blame] | 151 |  | 
| Felipe Monteiro | 2fe986d | 2018-03-20 21:53:51 +0000 | [diff] [blame] | 152 | @functools.wraps(test_func) | 
| DavidPurcell | 029d8c3 | 2017-01-06 15:27:41 -0500 | [diff] [blame] | 153 | def wrapper(*args, **kwargs): | 
| Felipe Monteiro | 78fc489 | 2017-04-12 21:33:39 +0100 | [diff] [blame] | 154 | 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`.') | 
| raiesmh08 | 8590c0c | 2017-03-14 18:06:52 +0530 | [diff] [blame] | 160 |  | 
| Felipe Monteiro | 44d7784 | 2018-03-21 02:42:59 +0000 | [diff] [blame] | 161 | 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 Monteiro | d5d76b8 | 2017-03-20 23:18:50 +0000 | [diff] [blame] | 169 |  | 
| Cliff Parsons | 35a7711 | 2018-05-07 14:03:40 -0500 | [diff] [blame] | 170 | 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 Bartra | 1299894 | 2017-03-17 17:35:45 -0400 | [diff] [blame] | 180 | expected_exception, irregular_msg = _get_exception_type( | 
| Cliff Parsons | 35a7711 | 2018-05-07 14:03:40 -0500 | [diff] [blame] | 181 | exp_error_code) | 
| DavidPurcell | 029d8c3 | 2017-01-06 15:27:41 -0500 | [diff] [blame] | 182 |  | 
| Mykola Yakovliev | 11376ab | 2018-08-06 15:34:22 -0500 | [diff] [blame] | 183 | caught_exception = None | 
| Sean Pryor | 7f8993f | 2017-08-14 12:53:17 -0400 | [diff] [blame] | 184 | test_status = 'Allowed' | 
|  | 185 |  | 
| DavidPurcell | 029d8c3 | 2017-01-06 15:27:41 -0500 | [diff] [blame] | 186 | try: | 
| Sean Pryor | 7f8993f | 2017-08-14 12:53:17 -0400 | [diff] [blame] | 187 | test_func(*args, **kwargs) | 
| Felipe Monteiro | 51299a1 | 2018-06-28 20:03:27 -0400 | [diff] [blame] | 188 | 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 Blanco | 36bea05 | 2017-07-19 12:01:59 -0400 | [diff] [blame] | 195 | except (expected_exception, | 
|  | 196 | rbac_exceptions.RbacConflictingPolicies, | 
| Mykola Yakovliev | 11376ab | 2018-08-06 15:34:22 -0500 | [diff] [blame] | 197 | rbac_exceptions.RbacMalformedResponse) as actual_exception: | 
|  | 198 | caught_exception = actual_exception | 
| Sean Pryor | 7f8993f | 2017-08-14 12:53:17 -0400 | [diff] [blame] | 199 | test_status = 'Denied' | 
| Mykola Yakovliev | 11376ab | 2018-08-06 15:34:22 -0500 | [diff] [blame] | 200 |  | 
| Felipe Monteiro | 8eda8cc | 2017-03-22 14:15:14 +0000 | [diff] [blame] | 201 | if irregular_msg: | 
| Felipe Monteiro | c0cb7eb | 2018-06-19 19:50:36 -0400 | [diff] [blame] | 202 | LOG.warning(irregular_msg, | 
|  | 203 | test_func.__name__, | 
|  | 204 | ', '.join(rules), | 
|  | 205 | service) | 
| Mykola Yakovliev | 11376ab | 2018-08-06 15:34:22 -0500 | [diff] [blame] | 206 |  | 
| DavidPurcell | 029d8c3 | 2017-01-06 15:27:41 -0500 | [diff] [blame] | 207 | if allowed: | 
| Felipe Monteiro | 44d7784 | 2018-03-21 02:42:59 +0000 | [diff] [blame] | 208 | 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))) | 
| DavidPurcell | 029d8c3 | 2017-01-06 15:27:41 -0500 | [diff] [blame] | 214 | LOG.error(msg) | 
| Felipe Monteiro | f16b6b3 | 2018-06-28 19:32:59 -0400 | [diff] [blame] | 215 | raise rbac_exceptions.RbacUnderPermissionException( | 
| Mykola Yakovliev | 11376ab | 2018-08-06 15:34:22 -0500 | [diff] [blame] | 216 | "%s Exception was: %s" % (msg, actual_exception)) | 
| Felipe Monteiro | f16b6b3 | 2018-06-28 19:32:59 -0400 | [diff] [blame] | 217 | except Exception as actual_exception: | 
| Mykola Yakovliev | 11376ab | 2018-08-06 15:34:22 -0500 | [diff] [blame] | 218 | caught_exception = actual_exception | 
|  | 219 |  | 
| Felipe Monteiro | f16b6b3 | 2018-06-28 19:32:59 -0400 | [diff] [blame] | 220 | 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) | 
| DavidPurcell | 029d8c3 | 2017-01-06 15:27:41 -0500 | [diff] [blame] | 239 | else: | 
|  | 240 | if not allowed: | 
| Felipe Monteiro | 44d7784 | 2018-03-21 02:42:59 +0000 | [diff] [blame] | 241 | 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 Monteiro | f16b6b3 | 2018-06-28 19:32:59 -0400 | [diff] [blame] | 248 | raise rbac_exceptions.RbacOverPermissionException(msg) | 
| raiesmh08 | 8590c0c | 2017-03-14 18:06:52 +0530 | [diff] [blame] | 249 | finally: | 
| Sean Pryor | 7f8993f | 2017-08-14 12:53:17 -0400 | [diff] [blame] | 250 | if CONF.patrole_log.enable_reporting: | 
|  | 251 | RBACLOG.info( | 
| Felipe Monteiro | c0cb7eb | 2018-06-19 19:50:36 -0400 | [diff] [blame] | 252 | "[Service]: %s, [Test]: %s, [Rules]: %s, " | 
| Sean Pryor | 7f8993f | 2017-08-14 12:53:17 -0400 | [diff] [blame] | 253 | "[Expected]: %s, [Actual]: %s", | 
| Felipe Monteiro | c0cb7eb | 2018-06-19 19:50:36 -0400 | [diff] [blame] | 254 | service, test_func.__name__, ', '.join(rules), | 
| Sean Pryor | 7f8993f | 2017-08-14 12:53:17 -0400 | [diff] [blame] | 255 | "Allowed" if allowed else "Denied", | 
|  | 256 | test_status) | 
| Felipe Monteiro | 78fc489 | 2017-04-12 21:33:39 +0100 | [diff] [blame] | 257 |  | 
| Mykola Yakovliev | 11376ab | 2018-08-06 15:34:22 -0500 | [diff] [blame] | 258 | # 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 Monteiro | 2fe986d | 2018-03-20 21:53:51 +0000 | [diff] [blame] | 266 | return wrapper | 
| DavidPurcell | 029d8c3 | 2017-01-06 15:27:41 -0500 | [diff] [blame] | 267 | return decorator | 
| Rick Bartra | 1299894 | 2017-03-17 17:35:45 -0400 | [diff] [blame] | 268 |  | 
|  | 269 |  | 
| Cliff Parsons | 35a7711 | 2018-05-07 14:03:40 -0500 | [diff] [blame] | 270 | def _prepare_multi_policy(rule, rules, exp_error_code, exp_error_codes): | 
| Cliff Parsons | 35a7711 | 2018-05-07 14:03:40 -0500 | [diff] [blame] | 271 | 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 Monteiro | 44d7784 | 2018-03-21 02:42:59 +0000 | [diff] [blame] | 295 | 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 Parsons | 35a7711 | 2018-05-07 14:03:40 -0500 | [diff] [blame] | 308 |  | 
|  | 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 Monteiro | 44d7784 | 2018-03-21 02:42:59 +0000 | [diff] [blame] | 320 |  | 
|  | 321 |  | 
| Felipe Monteiro | 318a0bf | 2018-02-27 06:57:10 -0500 | [diff] [blame] | 322 | def _is_authorized(test_obj, service, rule, extra_target_data): | 
| Felipe Monteiro | dea1384 | 2017-07-05 04:11:18 +0100 | [diff] [blame] | 323 | """Validates whether current RBAC role has permission to do policy action. | 
|  | 324 |  | 
| Felipe Monteiro | f2b58d7 | 2017-08-31 22:40:36 +0100 | [diff] [blame] | 325 | :param test_obj: An instance or subclass of ``tempest.test.BaseTestCase``. | 
| Felipe Monteiro | 01d633b | 2017-08-16 20:17:26 +0100 | [diff] [blame] | 326 | :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 Monteiro | f2b58d7 | 2017-08-31 22:40:36 +0100 | [diff] [blame] | 329 | :param extra_target_data: Dictionary, keyed with ``oslo.policy`` generic | 
| Felipe Monteiro | 01d633b | 2017-08-16 20:17:26 +0100 | [diff] [blame] | 330 | check names, whose values are string literals that reference nested | 
| Felipe Monteiro | f2b58d7 | 2017-08-31 22:40:36 +0100 | [diff] [blame] | 331 | ``tempest.test.BaseTestCase`` attributes. Used by ``oslo.policy`` for | 
| Felipe Monteiro | 01d633b | 2017-08-16 20:17:26 +0100 | [diff] [blame] | 332 | performing matching against attributes that are sent along with the API | 
|  | 333 | calls. | 
| Sean Pryor | 7f8993f | 2017-08-14 12:53:17 -0400 | [diff] [blame] | 334 |  | 
| Felipe Monteiro | 01d633b | 2017-08-16 20:17:26 +0100 | [diff] [blame] | 335 | :returns: True if the current RBAC role can perform the policy action, | 
|  | 336 | else False. | 
| Sean Pryor | 7f8993f | 2017-08-14 12:53:17 -0400 | [diff] [blame] | 337 |  | 
| Felipe Monteiro | 01d633b | 2017-08-16 20:17:26 +0100 | [diff] [blame] | 338 | :raises RbacResourceSetupFailed: If `project_id` or `user_id` are missing | 
|  | 339 | from the `auth_provider` attribute in `test_obj`. | 
| Felipe Monteiro | dea1384 | 2017-07-05 04:11:18 +0100 | [diff] [blame] | 340 | """ | 
| Sean Pryor | 7f8993f | 2017-08-14 12:53:17 -0400 | [diff] [blame] | 341 |  | 
| Felipe Monteiro | 78fc489 | 2017-04-12 21:33:39 +0100 | [diff] [blame] | 342 | try: | 
| Felipe Monteiro | e8d93e0 | 2017-07-19 20:52:20 +0100 | [diff] [blame] | 343 | project_id = test_obj.os_primary.credentials.project_id | 
|  | 344 | user_id = test_obj.os_primary.credentials.user_id | 
| Felipe Monteiro | 78fc489 | 2017-04-12 21:33:39 +0100 | [diff] [blame] | 345 | except AttributeError as e: | 
| Felipe Monteiro | e8d93e0 | 2017-07-19 20:52:20 +0100 | [diff] [blame] | 346 | msg = ("{0}: project_id or user_id not found in os_primary.credentials" | 
|  | 347 | .format(e)) | 
| Felipe Monteiro | 78fc489 | 2017-04-12 21:33:39 +0100 | [diff] [blame] | 348 | LOG.error(msg) | 
|  | 349 | raise rbac_exceptions.RbacResourceSetupFailed(msg) | 
|  | 350 |  | 
| Felipe Monteiro | 4ef7e53 | 2018-03-11 07:17:11 -0400 | [diff] [blame] | 351 | 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 Monteiro | 78fc489 | 2017-04-12 21:33:39 +0100 | [diff] [blame] | 363 |  | 
| Felipe Monteiro | 4ef7e53 | 2018-03-11 07:17:11 -0400 | [diff] [blame] | 364 | if is_allowed: | 
| Felipe Monteiro | c0cb7eb | 2018-06-19 19:50:36 -0400 | [diff] [blame] | 365 | LOG.debug("[Policy action]: %s, [Role]: %s is allowed!", rule, | 
| Felipe Monteiro | 4ef7e53 | 2018-03-11 07:17:11 -0400 | [diff] [blame] | 366 | role) | 
|  | 367 | else: | 
| Felipe Monteiro | c0cb7eb | 2018-06-19 19:50:36 -0400 | [diff] [blame] | 368 | LOG.debug("[Policy action]: %s, [Role]: %s is NOT allowed!", | 
| Felipe Monteiro | 4ef7e53 | 2018-03-11 07:17:11 -0400 | [diff] [blame] | 369 | rule, role) | 
|  | 370 |  | 
|  | 371 | return is_allowed | 
| Felipe Monteiro | 78fc489 | 2017-04-12 21:33:39 +0100 | [diff] [blame] | 372 |  | 
|  | 373 |  | 
| Felipe Monteiro | c0cb7eb | 2018-06-19 19:50:36 -0400 | [diff] [blame] | 374 | def _get_exception_type(expected_error_code=_DEFAULT_ERROR_CODE): | 
| Felipe Monteiro | 973a1bc | 2017-06-14 21:23:54 +0100 | [diff] [blame] | 375 | """Dynamically calculate the expected exception to be caught. | 
|  | 376 |  | 
|  | 377 | Dynamically calculate the expected exception to be caught by the test case. | 
| Felipe Monteiro | f2b58d7 | 2017-08-31 22:40:36 +0100 | [diff] [blame] | 378 | Only ``Forbidden`` and ``NotFound`` exceptions are permitted. ``NotFound`` | 
|  | 379 | is supported because Neutron, for security reasons, masks ``Forbidden`` | 
|  | 380 | exceptions as ``NotFound`` exceptions. | 
| Felipe Monteiro | 973a1bc | 2017-06-14 21:23:54 +0100 | [diff] [blame] | 381 |  | 
|  | 382 | :param expected_error_code: the integer representation of the expected | 
| Felipe Monteiro | f2b58d7 | 2017-08-31 22:40:36 +0100 | [diff] [blame] | 383 | exception to be caught. Must be contained in | 
|  | 384 | ``_SUPPORTED_ERROR_CODES``. | 
| Felipe Monteiro | 973a1bc | 2017-06-14 21:23:54 +0100 | [diff] [blame] | 385 | :returns: tuple of the exception type corresponding to | 
| Felipe Monteiro | f2b58d7 | 2017-08-31 22:40:36 +0100 | [diff] [blame] | 386 | ``expected_error_code`` and a message explaining that a non-Forbidden | 
| Felipe Monteiro | 973a1bc | 2017-06-14 21:23:54 +0100 | [diff] [blame] | 387 | exception was expected, if applicable. | 
|  | 388 | """ | 
| Rick Bartra | 1299894 | 2017-03-17 17:35:45 -0400 | [diff] [blame] | 389 | expected_exception = None | 
|  | 390 | irregular_msg = None | 
| Felipe Monteiro | 973a1bc | 2017-06-14 21:23:54 +0100 | [diff] [blame] | 391 |  | 
|  | 392 | if not isinstance(expected_error_code, six.integer_types) \ | 
| Sean Pryor | 7f8993f | 2017-08-14 12:53:17 -0400 | [diff] [blame] | 393 | or expected_error_code not in _SUPPORTED_ERROR_CODES: | 
| Felipe Monteiro | 973a1bc | 2017-06-14 21:23:54 +0100 | [diff] [blame] | 394 | 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 Monteiro | 78fc489 | 2017-04-12 21:33:39 +0100 | [diff] [blame] | 398 |  | 
| Rick Bartra | 1299894 | 2017-03-17 17:35:45 -0400 | [diff] [blame] | 399 | if expected_error_code == 403: | 
| Felipe Monteiro | 51299a1 | 2018-06-28 20:03:27 -0400 | [diff] [blame] | 400 | expected_exception = lib_exc.Forbidden | 
| Rick Bartra | 1299894 | 2017-03-17 17:35:45 -0400 | [diff] [blame] | 401 | elif expected_error_code == 404: | 
| Felipe Monteiro | 51299a1 | 2018-06-28 20:03:27 -0400 | [diff] [blame] | 402 | expected_exception = lib_exc.NotFound | 
| Felipe Monteiro | c0cb7eb | 2018-06-19 19:50:36 -0400 | [diff] [blame] | 403 | 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 Yakovliev | 11376ab | 2018-08-06 15:34:22 -0500 | [diff] [blame] | 406 | "irregular") | 
| Rick Bartra | 1299894 | 2017-03-17 17:35:45 -0400 | [diff] [blame] | 407 | return expected_exception, irregular_msg | 
| Felipe Monteiro | fd1db98 | 2017-04-13 21:19:41 +0100 | [diff] [blame] | 408 |  | 
|  | 409 |  | 
|  | 410 | def _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 Monteiro | 01d633b | 2017-08-16 20:17:26 +0100 | [diff] [blame] | 414 | policy string like "trust.trustor_user_id" to a nested list of | 
| Felipe Monteiro | f2b58d7 | 2017-08-31 22:40:36 +0100 | [diff] [blame] | 415 | ``tempest.test.BaseTestCase`` attributes. For example, the attribute list | 
| Masayuki Igawa | 80b9aab | 2018-01-09 17:00:45 +0900 | [diff] [blame] | 416 | in:: | 
| Felipe Monteiro | fd1db98 | 2017-04-13 21:19:41 +0100 | [diff] [blame] | 417 |  | 
| Masayuki Igawa | 80b9aab | 2018-01-09 17:00:45 +0900 | [diff] [blame] | 418 | "trust.trustor_user_id": "os.auth_provider.credentials.user_id" | 
| Felipe Monteiro | fd1db98 | 2017-04-13 21:19:41 +0100 | [diff] [blame] | 419 |  | 
| Felipe Monteiro | 01d633b | 2017-08-16 20:17:26 +0100 | [diff] [blame] | 420 | is parsed by iteratively calling ``getattr`` until the value of "user_id" | 
| Masayuki Igawa | 80b9aab | 2018-01-09 17:00:45 +0900 | [diff] [blame] | 421 | is resolved. The resulting dictionary returns:: | 
| Felipe Monteiro | fd1db98 | 2017-04-13 21:19:41 +0100 | [diff] [blame] | 422 |  | 
| Masayuki Igawa | 80b9aab | 2018-01-09 17:00:45 +0900 | [diff] [blame] | 423 | "trust.trustor_user_id": "the user_id of the `os_primary` credential" | 
| Felipe Monteiro | fd1db98 | 2017-04-13 21:19:41 +0100 | [diff] [blame] | 424 |  | 
| Felipe Monteiro | f2b58d7 | 2017-08-31 22:40:36 +0100 | [diff] [blame] | 425 | :param test_obj: An instance or subclass of ``tempest.test.BaseTestCase``. | 
|  | 426 | :param extra_target_data: Dictionary, keyed with ``oslo.policy`` generic | 
| Felipe Monteiro | 01d633b | 2017-08-16 20:17:26 +0100 | [diff] [blame] | 427 | check names, whose values are string literals that reference nested | 
| Felipe Monteiro | f2b58d7 | 2017-08-31 22:40:36 +0100 | [diff] [blame] | 428 | ``tempest.test.BaseTestCase`` attributes. Used by ``oslo.policy`` for | 
| Felipe Monteiro | 01d633b | 2017-08-16 20:17:26 +0100 | [diff] [blame] | 429 | performing matching against attributes that are sent along with the API | 
|  | 430 | calls. | 
|  | 431 | :returns: Dictionary containing additional object data needed by | 
| Felipe Monteiro | f2b58d7 | 2017-08-31 22:40:36 +0100 | [diff] [blame] | 432 | ``oslo.policy`` to validate generic checks. | 
| Felipe Monteiro | fd1db98 | 2017-04-13 21:19:41 +0100 | [diff] [blame] | 433 | """ | 
|  | 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 Monteiro | f16b6b3 | 2018-06-28 19:32:59 -0400 | [diff] [blame] | 444 |  | 
|  | 445 |  | 
|  | 446 | def _check_for_expected_mismatch_exception(expected_exception, | 
|  | 447 | actual_exception): | 
| Mykola Yakovliev | 11376ab | 2018-08-06 15:34:22 -0500 | [diff] [blame] | 448 | """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 Monteiro | 51299a1 | 2018-06-28 20:03:27 -0400 | [diff] [blame] | 458 | permission_exceptions = (lib_exc.Forbidden, lib_exc.NotFound) | 
| Felipe Monteiro | f16b6b3 | 2018-06-28 19:32:59 -0400 | [diff] [blame] | 459 | if isinstance(actual_exception, permission_exceptions): | 
|  | 460 | if not isinstance(actual_exception, expected_exception.__class__): | 
|  | 461 | return True | 
|  | 462 | return False | 
| Mykola Yakovliev | 11376ab | 2018-08-06 15:34:22 -0500 | [diff] [blame] | 463 |  | 
|  | 464 |  | 
|  | 465 | def _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) |