blob: d06986ab6faeca4a89f6086cb16d3ece42602697 [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 Monteirob0595652017-01-23 16:51:58 -050016import logging
Felipe Monteiro8eda8cc2017-03-22 14:15:14 +000017import sys
Felipe Monteiro78fc4892017-04-12 21:33:39 +010018import testtools
Felipe Monteiro8eda8cc2017-03-22 14:15:14 +000019
20import six
Felipe Monteirob0595652017-01-23 16:51:58 -050021
DavidPurcell029d8c32017-01-06 15:27:41 -050022from tempest import config
23from tempest.lib import exceptions
raiesmh088590c0c2017-03-14 18:06:52 +053024from tempest import test
DavidPurcell029d8c32017-01-06 15:27:41 -050025
DavidPurcell029d8c32017-01-06 15:27:41 -050026from patrole_tempest_plugin import rbac_exceptions
Felipe Monteiro78fc4892017-04-12 21:33:39 +010027from patrole_tempest_plugin import rbac_policy_parser
Felipe Monteiro8a043fb2017-08-06 06:29:05 +010028from patrole_tempest_plugin import rbac_utils
Rick Bartraed950052017-06-29 17:20:33 -040029from patrole_tempest_plugin import requirements_authority
DavidPurcell029d8c32017-01-06 15:27:41 -050030
31CONF = config.CONF
32LOG = logging.getLogger(__name__)
33
Felipe Monteiro973a1bc2017-06-14 21:23:54 +010034_SUPPORTED_ERROR_CODES = [403, 404]
35
DavidPurcell029d8c32017-01-06 15:27:41 -050036
Felipe Monteiroe7e552e2017-05-02 17:04:12 +010037def action(service, rule='', admin_only=False, expected_error_code=403,
Felipe Monteiro0854ded2017-05-05 16:30:55 +010038 extra_target_data=None):
Felipe Monteiro01d633b2017-08-16 20:17:26 +010039 """A decorator for verifying policy enforcement.
Felipe Monteirod5d76b82017-03-20 23:18:50 +000040
Felipe Monteiro01d633b2017-08-16 20:17:26 +010041 A decorator which allows for positive and negative RBAC testing. Given:
Rick Bartraed950052017-06-29 17:20:33 -040042
Felipe Monteiro01d633b2017-08-16 20:17:26 +010043 * an OpenStack service,
44 * a policy action (``rule``) enforced by that service, and
45 * the test role defined by ``[patrole] rbac_test_role``
Felipe Monteirod5d76b82017-03-20 23:18:50 +000046
Felipe Monteiro01d633b2017-08-16 20:17:26 +010047 determines whether the test role has sufficient permissions to perform an
48 API call that enforces the ``rule``.
Felipe Monteirod5d76b82017-03-20 23:18:50 +000049
Felipe Monteiro01d633b2017-08-16 20:17:26 +010050 This decorator should only be applied to an instance or subclass of
51 `tempest.base.BaseTestCase`.
52
53 The result from ``_is_authorized`` is used to determine the *expected*
54 test result. The *actual* test result is determined by running the
55 Tempest test this decorator applies to.
56
57 Below are the following possibilities from comparing the *expected* and
58 *actual* results:
59
60 1) If *expected* is True and the test passes (*actual*), this is a success.
61 2) If *expected* is True and the test fails (*actual*), this results in a
62 `Forbidden` exception failure.
63 3) If *expected* is False and the test passes (*actual*), this results in
64 an `OverPermission` exception failure.
65 4) If *expected* is False and the test fails (*actual*), this is a success.
66
67 As such, negative and positive testing can be applied using this decorator.
68
69 :param service: A OpenStack service. Examples: "nova" or "neutron".
70 :param rule: A policy action defined in a policy.json file (or in
71 code).
72
73 .. note::
74
75 Patrole currently only supports custom JSON policy files.
76
77 :param admin_only: Skips over `oslo.policy` check because the policy action
78 defined by `rule` is not enforced by the service's policy
79 enforcement engine. For example, Keystone v2 performs an admin check
80 for most of its endpoints. If True, `rule` is effectively
81 ignored.
Felipe Monteirod5d76b82017-03-20 23:18:50 +000082 :param expected_error_code: Overrides default value of 403 (Forbidden)
Felipe Monteiro973a1bc2017-06-14 21:23:54 +010083 with endpoint-specific error code. Currently only supports 403 and 404.
84 Support for 404 is needed because some services, like Neutron,
85 intentionally throw a 404 for security reasons.
Felipe Monteirod5d76b82017-03-20 23:18:50 +000086
Felipe Monteiro01d633b2017-08-16 20:17:26 +010087 .. warning::
88
89 A 404 should not be provided *unless* the endpoint masks a
90 `Forbidden` exception as a `Not Found` exception.
91
92 :param extra_target_data: Dictionary, keyed with `oslo.policy` generic
93 check names, whose values are string literals that reference nested
94 `tempest.base.BaseTestCase` attributes. Used by `oslo.policy` for
95 performing matching against attributes that are sent along with the API
96 calls. Example::
97
98 extra_target_data={
99 "target.token.user_id":
100 "os_alt.auth_provider.credentials.user_id"
101 })
102
103 :raises NotFound: If `service` is invalid or if Tempest credentials cannot
104 be found.
105 :raises Forbidden: For item (2) above.
106 :raises RbacOverPermission: For item (3) above.
107
108 Examples::
109
110 @rbac_rule_validation.action(
111 service="nova", rule="os_compute_api:os-agents")
112 def test_list_agents_rbac(self):
113 # The call to ``switch_role`` is mandatory.
114 self.rbac_utils.switch_role(self, toggle_rbac_role=True)
115 self.agents_client.list_agents()
Felipe Monteirod5d76b82017-03-20 23:18:50 +0000116 """
Felipe Monteiro0854ded2017-05-05 16:30:55 +0100117
118 if extra_target_data is None:
119 extra_target_data = {}
120
DavidPurcell029d8c32017-01-06 15:27:41 -0500121 def decorator(func):
Felipe Monteirof6eb8622017-08-06 06:08:02 +0100122 role = CONF.patrole.rbac_test_role
Felipe Monteiro78fc4892017-04-12 21:33:39 +0100123
DavidPurcell029d8c32017-01-06 15:27:41 -0500124 def wrapper(*args, **kwargs):
Felipe Monteiro78fc4892017-04-12 21:33:39 +0100125 if args and isinstance(args[0], test.BaseTestCase):
126 test_obj = args[0]
127 else:
128 raise rbac_exceptions.RbacResourceSetupFailed(
129 '`rbac_rule_validation` decorator can only be applied to '
130 'an instance of `tempest.test.BaseTestCase`.')
raiesmh088590c0c2017-03-14 18:06:52 +0530131
Felipe Monteirod5d76b82017-03-20 23:18:50 +0000132 if admin_only:
133 LOG.info("As admin_only is True, only admin role should be "
134 "allowed to perform the API. Skipping oslo.policy "
135 "check for policy action {0}.".format(rule))
Felipe Monteiro8a043fb2017-08-06 06:29:05 +0100136 allowed = rbac_utils.is_admin()
Felipe Monteirod5d76b82017-03-20 23:18:50 +0000137 else:
Felipe Monteiro78fc4892017-04-12 21:33:39 +0100138 allowed = _is_authorized(test_obj, service, rule,
139 extra_target_data)
Felipe Monteirod5d76b82017-03-20 23:18:50 +0000140
Rick Bartra12998942017-03-17 17:35:45 -0400141 expected_exception, irregular_msg = _get_exception_type(
142 expected_error_code)
DavidPurcell029d8c32017-01-06 15:27:41 -0500143
144 try:
Felipe Monteiro78fc4892017-04-12 21:33:39 +0100145 func(*args, **kwargs)
Rick Bartra503c5572017-03-09 13:49:58 -0500146 except rbac_exceptions.RbacInvalidService as e:
Felipe Monteiro48c913d2017-03-15 12:07:48 -0400147 msg = ("%s is not a valid service." % service)
148 LOG.error(msg)
149 raise exceptions.NotFound(
Felipe Monteiro78fc4892017-04-12 21:33:39 +0100150 "%s RbacInvalidService was: %s" % (msg, e))
Samantha Blanco36bea052017-07-19 12:01:59 -0400151 except (expected_exception,
152 rbac_exceptions.RbacConflictingPolicies,
153 rbac_exceptions.RbacMalformedResponse) as e:
Felipe Monteiro8eda8cc2017-03-22 14:15:14 +0000154 if irregular_msg:
155 LOG.warning(irregular_msg.format(rule, service))
DavidPurcell029d8c32017-01-06 15:27:41 -0500156 if allowed:
157 msg = ("Role %s was not allowed to perform %s." %
Felipe Monteiro78fc4892017-04-12 21:33:39 +0100158 (role, rule))
DavidPurcell029d8c32017-01-06 15:27:41 -0500159 LOG.error(msg)
160 raise exceptions.Forbidden(
Felipe Monteiro4bf66a22017-05-07 14:44:21 +0100161 "%s Exception was: %s" % (msg, e))
Felipe Monteiro8eda8cc2017-03-22 14:15:14 +0000162 except Exception as e:
163 exc_info = sys.exc_info()
164 error_details = exc_info[1].__str__()
165 msg = ("%s An unexpected exception has occurred: Expected "
166 "exception was %s, which was not thrown."
167 % (error_details, expected_exception.__name__))
168 LOG.error(msg)
169 six.reraise(exc_info[0], exc_info[0](msg), exc_info[2])
DavidPurcell029d8c32017-01-06 15:27:41 -0500170 else:
171 if not allowed:
Felipe Monteiro4bf66a22017-05-07 14:44:21 +0100172 LOG.error("Role %s was allowed to perform %s",
Felipe Monteiroe52cbc62017-05-24 17:48:59 +0100173 role, rule)
DavidPurcell029d8c32017-01-06 15:27:41 -0500174 raise rbac_exceptions.RbacOverPermission(
175 "OverPermission: Role %s was allowed to perform %s" %
Felipe Monteiro78fc4892017-04-12 21:33:39 +0100176 (role, rule))
raiesmh088590c0c2017-03-14 18:06:52 +0530177 finally:
Felipe Monteiro78fc4892017-04-12 21:33:39 +0100178 test_obj.rbac_utils.switch_role(test_obj,
179 toggle_rbac_role=False)
180
181 _wrapper = testtools.testcase.attr(role)(wrapper)
182 return _wrapper
DavidPurcell029d8c32017-01-06 15:27:41 -0500183 return decorator
Rick Bartra12998942017-03-17 17:35:45 -0400184
185
Felipe Monteiro01d633b2017-08-16 20:17:26 +0100186def _is_authorized(test_obj, service, rule, extra_target_data):
Felipe Monteirodea13842017-07-05 04:11:18 +0100187 """Validates whether current RBAC role has permission to do policy action.
188
Felipe Monteiro01d633b2017-08-16 20:17:26 +0100189 :param test_obj: An instance or subclass of `tempest.base.BaseTestCase`.
190 :param service: The OpenStack service that enforces ``rule``.
191 :param rule: The name of the policy action. Examples include
192 "identity:create_user" or "os_compute_api:os-agents".
193 :param extra_target_data: Dictionary, keyed with `oslo.policy` generic
194 check names, whose values are string literals that reference nested
195 `tempest.base.BaseTestCase` attributes. Used by `oslo.policy` for
196 performing matching against attributes that are sent along with the API
197 calls.
198 :returns: True if the current RBAC role can perform the policy action,
199 else False.
200 :raises RbacResourceSetupFailed: If `project_id` or `user_id` are missing
201 from the `auth_provider` attribute in `test_obj`.
202 :raises RbacParsingException: if ``[patrole] strict_policy_check`` is True
203 and the ``rule`` does not exist in the system.
204 :raises skipException: If ``[patrole] strict_policy_check`` is False and
205 the ``rule`` does not exist in the system.
Felipe Monteirodea13842017-07-05 04:11:18 +0100206 """
Felipe Monteiro78fc4892017-04-12 21:33:39 +0100207 try:
Felipe Monteiroe8d93e02017-07-19 20:52:20 +0100208 project_id = test_obj.os_primary.credentials.project_id
209 user_id = test_obj.os_primary.credentials.user_id
Felipe Monteiro78fc4892017-04-12 21:33:39 +0100210 except AttributeError as e:
Felipe Monteiroe8d93e02017-07-19 20:52:20 +0100211 msg = ("{0}: project_id or user_id not found in os_primary.credentials"
212 .format(e))
Felipe Monteiro78fc4892017-04-12 21:33:39 +0100213 LOG.error(msg)
214 raise rbac_exceptions.RbacResourceSetupFailed(msg)
215
216 try:
Felipe Monteirof6eb8622017-08-06 06:08:02 +0100217 role = CONF.patrole.rbac_test_role
Rick Bartraed950052017-06-29 17:20:33 -0400218 # Test RBAC against custom requirements. Otherwise use oslo.policy
Felipe Monteirof6eb8622017-08-06 06:08:02 +0100219 if CONF.patrole.test_custom_requirements:
Rick Bartraed950052017-06-29 17:20:33 -0400220 authority = requirements_authority.RequirementsAuthority(
Felipe Monteirof6eb8622017-08-06 06:08:02 +0100221 CONF.patrole.custom_requirements_file, service)
Rick Bartraed950052017-06-29 17:20:33 -0400222 else:
223 formatted_target_data = _format_extra_target_data(
224 test_obj, extra_target_data)
225 authority = rbac_policy_parser.RbacPolicyParser(
226 project_id, user_id, service,
227 extra_target_data=formatted_target_data)
Felipe Monteiro01d633b2017-08-16 20:17:26 +0100228 is_allowed = authority.allowed(rule, role)
Felipe Monteiro78fc4892017-04-12 21:33:39 +0100229
230 if is_allowed:
Felipe Monteiro01d633b2017-08-16 20:17:26 +0100231 LOG.debug("[Action]: %s, [Role]: %s is allowed!", rule,
Felipe Monteiro78fc4892017-04-12 21:33:39 +0100232 role)
233 else:
234 LOG.debug("[Action]: %s, [Role]: %s is NOT allowed!",
Felipe Monteiro01d633b2017-08-16 20:17:26 +0100235 rule, role)
Felipe Monteiro78fc4892017-04-12 21:33:39 +0100236 return is_allowed
237 except rbac_exceptions.RbacParsingException as e:
Felipe Monteirof6eb8622017-08-06 06:08:02 +0100238 if CONF.patrole.strict_policy_check:
Felipe Monteiro78fc4892017-04-12 21:33:39 +0100239 raise e
240 else:
241 raise testtools.TestCase.skipException(str(e))
242 return False
243
244
Felipe Monteiro973a1bc2017-06-14 21:23:54 +0100245def _get_exception_type(expected_error_code=403):
246 """Dynamically calculate the expected exception to be caught.
247
248 Dynamically calculate the expected exception to be caught by the test case.
249 Only `Forbidden` and `NotFound` exceptions are permitted. `NotFound` is
250 supported because Neutron, for security reasons, masks `Forbidden`
251 exceptions as `NotFound` exceptions.
252
253 :param expected_error_code: the integer representation of the expected
254 exception to be caught. Must be contained in `_SUPPORTED_ERROR_CODES`.
255 :returns: tuple of the exception type corresponding to
256 `expected_error_code` and a message explaining that a non-Forbidden
257 exception was expected, if applicable.
258 """
Rick Bartra12998942017-03-17 17:35:45 -0400259 expected_exception = None
260 irregular_msg = None
Felipe Monteiro973a1bc2017-06-14 21:23:54 +0100261
262 if not isinstance(expected_error_code, six.integer_types) \
263 or expected_error_code not in _SUPPORTED_ERROR_CODES:
264 msg = ("Please pass an expected error code. Currently "
265 "supported codes: {0}".format(_SUPPORTED_ERROR_CODES))
266 LOG.error(msg)
267 raise rbac_exceptions.RbacInvalidErrorCode(msg)
Felipe Monteiro78fc4892017-04-12 21:33:39 +0100268
Rick Bartra12998942017-03-17 17:35:45 -0400269 if expected_error_code == 403:
270 expected_exception = exceptions.Forbidden
271 elif expected_error_code == 404:
272 expected_exception = exceptions.NotFound
273 irregular_msg = ("NotFound exception was caught for policy action "
274 "{0}. The service {1} throws a 404 instead of a 403, "
275 "which is irregular.")
Rick Bartra12998942017-03-17 17:35:45 -0400276
277 return expected_exception, irregular_msg
Felipe Monteirofd1db982017-04-13 21:19:41 +0100278
279
280def _format_extra_target_data(test_obj, extra_target_data):
281 """Formats the "extra_target_data" dictionary with correct test data.
282
283 Before being formatted, "extra_target_data" is a dictionary that maps a
Felipe Monteiro01d633b2017-08-16 20:17:26 +0100284 policy string like "trust.trustor_user_id" to a nested list of
285 `tempest.base.BaseTestCase` attributes. For example, the attribute list in:
Felipe Monteirofd1db982017-04-13 21:19:41 +0100286
287 "trust.trustor_user_id": "os.auth_provider.credentials.user_id"
288
Felipe Monteiro01d633b2017-08-16 20:17:26 +0100289 is parsed by iteratively calling ``getattr`` until the value of "user_id"
Felipe Monteirofd1db982017-04-13 21:19:41 +0100290 is resolved. The resulting dictionary returns:
291
Felipe Monteiro01d633b2017-08-16 20:17:26 +0100292 "trust.trustor_user_id": "the user_id of the `os_primary` credential"
Felipe Monteirofd1db982017-04-13 21:19:41 +0100293
Felipe Monteiro01d633b2017-08-16 20:17:26 +0100294 :param test_obj: An instance or subclass of `tempest.base.BaseTestCase`.
295 :param extra_target_data: Dictionary, keyed with `oslo.policy` generic
296 check names, whose values are string literals that reference nested
297 `tempest.base.BaseTestCase` attributes. Used by `oslo.policy` for
298 performing matching against attributes that are sent along with the API
299 calls.
300 :returns: Dictionary containing additional object data needed by
301 `oslo.policy` to validate generic checks.
Felipe Monteirofd1db982017-04-13 21:19:41 +0100302 """
303 attr_value = test_obj
304 formatted_target_data = {}
305
306 for user_attribute, attr_string in extra_target_data.items():
307 attrs = attr_string.split('.')
308 for attr in attrs:
309 attr_value = getattr(attr_value, attr)
310 formatted_target_data[user_attribute] = attr_value
311
312 return formatted_target_data