blob: 69a43ea4c0280e4c11d6c51da8a9b5fb941b246c [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
25from tempest.lib import exceptions
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]
36
Sean Pryor7f8993f2017-08-14 12:53:17 -040037RBACLOG = logging.getLogger('rbac_reporting')
38
DavidPurcell029d8c32017-01-06 15:27:41 -050039
Felipe Monteiro44d77842018-03-21 02:42:59 +000040def action(service, rule='', rules=None, expected_error_code=403,
41 extra_target_data=None):
Felipe Monteirof2b58d72017-08-31 22:40:36 +010042 """A decorator for verifying OpenStack policy enforcement.
Felipe Monteirod5d76b82017-03-20 23:18:50 +000043
Felipe Monteiro01d633b2017-08-16 20:17:26 +010044 A decorator which allows for positive and negative RBAC testing. Given:
Rick Bartraed950052017-06-29 17:20:33 -040045
Masayuki Igawa80b9aab2018-01-09 17:00:45 +090046 * an OpenStack service,
47 * a policy action (``rule``) enforced by that service, and
48 * the test role defined by ``[patrole] rbac_test_role``
Felipe Monteirod5d76b82017-03-20 23:18:50 +000049
Felipe Monteiro01d633b2017-08-16 20:17:26 +010050 determines whether the test role has sufficient permissions to perform an
51 API call that enforces the ``rule``.
Felipe Monteirod5d76b82017-03-20 23:18:50 +000052
Felipe Monteiro01d633b2017-08-16 20:17:26 +010053 This decorator should only be applied to an instance or subclass of
Masayuki Igawa80b9aab2018-01-09 17:00:45 +090054 ``tempest.test.BaseTestCase``.
Felipe Monteiro01d633b2017-08-16 20:17:26 +010055
56 The result from ``_is_authorized`` is used to determine the *expected*
57 test result. The *actual* test result is determined by running the
58 Tempest test this decorator applies to.
59
60 Below are the following possibilities from comparing the *expected* and
61 *actual* results:
62
63 1) If *expected* is True and the test passes (*actual*), this is a success.
64 2) If *expected* is True and the test fails (*actual*), this results in a
65 `Forbidden` exception failure.
66 3) If *expected* is False and the test passes (*actual*), this results in
67 an `OverPermission` exception failure.
68 4) If *expected* is False and the test fails (*actual*), this is a success.
69
70 As such, negative and positive testing can be applied using this decorator.
71
Felipe Monteiro44d77842018-03-21 02:42:59 +000072 :param str service: An OpenStack service. Examples: "nova" or "neutron".
73 :param str rule: (DEPRECATED) A policy action defined in a policy.json file
74 or in code.
75 :param list rules: A list of policy actions defined in a policy.json file
76 or in code. The rules are logical-ANDed together to derive the expected
77 result.
Felipe Monteiro01d633b2017-08-16 20:17:26 +010078
79 .. note::
80
81 Patrole currently only supports custom JSON policy files.
82
Felipe Monteiro44d77842018-03-21 02:42:59 +000083 :param int expected_error_code: Overrides default value of 403 (Forbidden)
Felipe Monteiro973a1bc2017-06-14 21:23:54 +010084 with endpoint-specific error code. Currently only supports 403 and 404.
85 Support for 404 is needed because some services, like Neutron,
86 intentionally throw a 404 for security reasons.
Felipe Monteirod5d76b82017-03-20 23:18:50 +000087
Felipe Monteiro01d633b2017-08-16 20:17:26 +010088 .. warning::
89
90 A 404 should not be provided *unless* the endpoint masks a
Felipe Monteirof2b58d72017-08-31 22:40:36 +010091 ``Forbidden`` exception as a ``NotFound`` exception.
Felipe Monteiro01d633b2017-08-16 20:17:26 +010092
Felipe Monteiro44d77842018-03-21 02:42:59 +000093 :param dict extra_target_data: Dictionary, keyed with ``oslo.policy``
94 generic check names, whose values are string literals that reference
95 nested ``tempest.test.BaseTestCase`` attributes. Used by
96 ``oslo.policy`` for performing matching against attributes that are
97 sent along with the API calls. Example::
Felipe Monteiro01d633b2017-08-16 20:17:26 +010098
99 extra_target_data={
100 "target.token.user_id":
101 "os_alt.auth_provider.credentials.user_id"
102 })
103
Felipe Monteirof2b58d72017-08-31 22:40:36 +0100104 :raises NotFound: If ``service`` is invalid.
Felipe Monteiro01d633b2017-08-16 20:17:26 +0100105 :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):
Felipe Monteiro1c8620a2018-02-25 18:52:22 +0000113 # The call to `override_role` is mandatory.
114 with self.rbac_utils.override_role(self):
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
Felipe Monteiro44d77842018-03-21 02:42:59 +0000121 rules = _prepare_rules(rule, rules)
122
Sean Pryor7f8993f2017-08-14 12:53:17 -0400123 def decorator(test_func):
Felipe Monteirof6eb8622017-08-06 06:08:02 +0100124 role = CONF.patrole.rbac_test_role
Felipe Monteiro78fc4892017-04-12 21:33:39 +0100125
Felipe Monteiro2fe986d2018-03-20 21:53:51 +0000126 @functools.wraps(test_func)
DavidPurcell029d8c32017-01-06 15:27:41 -0500127 def wrapper(*args, **kwargs):
Felipe Monteiro78fc4892017-04-12 21:33:39 +0100128 if args and isinstance(args[0], test.BaseTestCase):
129 test_obj = args[0]
130 else:
131 raise rbac_exceptions.RbacResourceSetupFailed(
132 '`rbac_rule_validation` decorator can only be applied to '
133 'an instance of `tempest.test.BaseTestCase`.')
raiesmh088590c0c2017-03-14 18:06:52 +0530134
Felipe Monteiro44d77842018-03-21 02:42:59 +0000135 allowed = True
136 disallowed_rules = []
137 for rule in rules:
138 _allowed = _is_authorized(
139 test_obj, service, rule, extra_target_data)
140 if not _allowed:
141 disallowed_rules.append(rule)
142 allowed = allowed and _allowed
Felipe Monteirod5d76b82017-03-20 23:18:50 +0000143
Rick Bartra12998942017-03-17 17:35:45 -0400144 expected_exception, irregular_msg = _get_exception_type(
145 expected_error_code)
DavidPurcell029d8c32017-01-06 15:27:41 -0500146
Sean Pryor7f8993f2017-08-14 12:53:17 -0400147 test_status = 'Allowed'
148
DavidPurcell029d8c32017-01-06 15:27:41 -0500149 try:
Sean Pryor7f8993f2017-08-14 12:53:17 -0400150 test_func(*args, **kwargs)
Rick Bartra503c5572017-03-09 13:49:58 -0500151 except rbac_exceptions.RbacInvalidService as e:
Felipe Monteiro48c913d2017-03-15 12:07:48 -0400152 msg = ("%s is not a valid service." % service)
Sean Pryor7f8993f2017-08-14 12:53:17 -0400153 test_status = ('Error, %s' % (msg))
Felipe Monteiro48c913d2017-03-15 12:07:48 -0400154 LOG.error(msg)
155 raise exceptions.NotFound(
Felipe Monteiro78fc4892017-04-12 21:33:39 +0100156 "%s RbacInvalidService was: %s" % (msg, e))
Samantha Blanco36bea052017-07-19 12:01:59 -0400157 except (expected_exception,
158 rbac_exceptions.RbacConflictingPolicies,
159 rbac_exceptions.RbacMalformedResponse) as e:
Sean Pryor7f8993f2017-08-14 12:53:17 -0400160 test_status = 'Denied'
Felipe Monteiro8eda8cc2017-03-22 14:15:14 +0000161 if irregular_msg:
162 LOG.warning(irregular_msg.format(rule, service))
DavidPurcell029d8c32017-01-06 15:27:41 -0500163 if allowed:
Felipe Monteiro44d77842018-03-21 02:42:59 +0000164 msg = ("Role %s was not allowed to perform the following "
165 "actions: %s. Expected allowed actions: %s. "
166 "Expected disallowed actions: %s." % (
167 role, sorted(rules),
168 sorted(set(rules) - set(disallowed_rules)),
169 sorted(disallowed_rules)))
DavidPurcell029d8c32017-01-06 15:27:41 -0500170 LOG.error(msg)
171 raise exceptions.Forbidden(
Felipe Monteiro4bf66a22017-05-07 14:44:21 +0100172 "%s Exception was: %s" % (msg, e))
Felipe Monteiro8eda8cc2017-03-22 14:15:14 +0000173 except Exception as e:
Felipe Monteiro38f344b2017-11-03 12:59:15 +0000174 with excutils.save_and_reraise_exception():
175 exc_info = sys.exc_info()
176 error_details = six.text_type(exc_info[1])
177 msg = ("An unexpected exception has occurred during test: "
178 "%s. Exception was: %s" % (test_func.__name__,
179 error_details))
180 test_status = 'Error, %s' % (error_details)
181 LOG.error(msg)
DavidPurcell029d8c32017-01-06 15:27:41 -0500182 else:
183 if not allowed:
Felipe Monteiro44d77842018-03-21 02:42:59 +0000184 msg = (
185 "OverPermission: Role %s was allowed to perform the "
186 "following disallowed actions: %s" % (
187 role, sorted(disallowed_rules)
188 )
189 )
190 LOG.error(msg)
191 raise rbac_exceptions.RbacOverPermission(msg)
raiesmh088590c0c2017-03-14 18:06:52 +0530192 finally:
Sean Pryor7f8993f2017-08-14 12:53:17 -0400193 if CONF.patrole_log.enable_reporting:
194 RBACLOG.info(
195 "[Service]: %s, [Test]: %s, [Rule]: %s, "
196 "[Expected]: %s, [Actual]: %s",
197 service, test_func.__name__, rule,
198 "Allowed" if allowed else "Denied",
199 test_status)
Felipe Monteiro78fc4892017-04-12 21:33:39 +0100200
Felipe Monteiro2fe986d2018-03-20 21:53:51 +0000201 return wrapper
DavidPurcell029d8c32017-01-06 15:27:41 -0500202 return decorator
Rick Bartra12998942017-03-17 17:35:45 -0400203
204
Felipe Monteiro44d77842018-03-21 02:42:59 +0000205def _prepare_rules(rule, rules):
206 if rules is None:
207 rules = []
208 elif not isinstance(rules, (tuple, list)):
209 rules = [rules]
210 if rule:
211 deprecation_msg = (
212 "The `rule` argument has been deprecated in favor of `rules` "
213 "and will be removed in a future version.")
214 versionutils.report_deprecated_feature(LOG, deprecation_msg)
215 if rules:
216 LOG.debug("The `rules` argument will be used instead of `rule`.")
217 else:
218 rules.append(rule)
219 return rules
220
221
Felipe Monteiro318a0bf2018-02-27 06:57:10 -0500222def _is_authorized(test_obj, service, rule, extra_target_data):
Felipe Monteirodea13842017-07-05 04:11:18 +0100223 """Validates whether current RBAC role has permission to do policy action.
224
Felipe Monteirof2b58d72017-08-31 22:40:36 +0100225 :param test_obj: An instance or subclass of ``tempest.test.BaseTestCase``.
Felipe Monteiro01d633b2017-08-16 20:17:26 +0100226 :param service: The OpenStack service that enforces ``rule``.
227 :param rule: The name of the policy action. Examples include
228 "identity:create_user" or "os_compute_api:os-agents".
Felipe Monteirof2b58d72017-08-31 22:40:36 +0100229 :param extra_target_data: Dictionary, keyed with ``oslo.policy`` generic
Felipe Monteiro01d633b2017-08-16 20:17:26 +0100230 check names, whose values are string literals that reference nested
Felipe Monteirof2b58d72017-08-31 22:40:36 +0100231 ``tempest.test.BaseTestCase`` attributes. Used by ``oslo.policy`` for
Felipe Monteiro01d633b2017-08-16 20:17:26 +0100232 performing matching against attributes that are sent along with the API
233 calls.
Sean Pryor7f8993f2017-08-14 12:53:17 -0400234
Felipe Monteiro01d633b2017-08-16 20:17:26 +0100235 :returns: True if the current RBAC role can perform the policy action,
236 else False.
Sean Pryor7f8993f2017-08-14 12:53:17 -0400237
Felipe Monteiro01d633b2017-08-16 20:17:26 +0100238 :raises RbacResourceSetupFailed: If `project_id` or `user_id` are missing
239 from the `auth_provider` attribute in `test_obj`.
Felipe Monteirodea13842017-07-05 04:11:18 +0100240 """
Sean Pryor7f8993f2017-08-14 12:53:17 -0400241
Felipe Monteiro78fc4892017-04-12 21:33:39 +0100242 try:
Felipe Monteiroe8d93e02017-07-19 20:52:20 +0100243 project_id = test_obj.os_primary.credentials.project_id
244 user_id = test_obj.os_primary.credentials.user_id
Felipe Monteiro78fc4892017-04-12 21:33:39 +0100245 except AttributeError as e:
Felipe Monteiroe8d93e02017-07-19 20:52:20 +0100246 msg = ("{0}: project_id or user_id not found in os_primary.credentials"
247 .format(e))
Felipe Monteiro78fc4892017-04-12 21:33:39 +0100248 LOG.error(msg)
249 raise rbac_exceptions.RbacResourceSetupFailed(msg)
250
Felipe Monteiro4ef7e532018-03-11 07:17:11 -0400251 role = CONF.patrole.rbac_test_role
252 # Test RBAC against custom requirements. Otherwise use oslo.policy.
253 if CONF.patrole.test_custom_requirements:
254 authority = requirements_authority.RequirementsAuthority(
255 CONF.patrole.custom_requirements_file, service)
256 else:
257 formatted_target_data = _format_extra_target_data(
258 test_obj, extra_target_data)
259 authority = policy_authority.PolicyAuthority(
260 project_id, user_id, service,
261 extra_target_data=formatted_target_data)
262 is_allowed = authority.allowed(rule, role)
Felipe Monteiro78fc4892017-04-12 21:33:39 +0100263
Felipe Monteiro4ef7e532018-03-11 07:17:11 -0400264 if is_allowed:
265 LOG.debug("[Action]: %s, [Role]: %s is allowed!", rule,
266 role)
267 else:
268 LOG.debug("[Action]: %s, [Role]: %s is NOT allowed!",
269 rule, role)
270
271 return is_allowed
Felipe Monteiro78fc4892017-04-12 21:33:39 +0100272
273
Felipe Monteiro973a1bc2017-06-14 21:23:54 +0100274def _get_exception_type(expected_error_code=403):
275 """Dynamically calculate the expected exception to be caught.
276
277 Dynamically calculate the expected exception to be caught by the test case.
Felipe Monteirof2b58d72017-08-31 22:40:36 +0100278 Only ``Forbidden`` and ``NotFound`` exceptions are permitted. ``NotFound``
279 is supported because Neutron, for security reasons, masks ``Forbidden``
280 exceptions as ``NotFound`` exceptions.
Felipe Monteiro973a1bc2017-06-14 21:23:54 +0100281
282 :param expected_error_code: the integer representation of the expected
Felipe Monteirof2b58d72017-08-31 22:40:36 +0100283 exception to be caught. Must be contained in
284 ``_SUPPORTED_ERROR_CODES``.
Felipe Monteiro973a1bc2017-06-14 21:23:54 +0100285 :returns: tuple of the exception type corresponding to
Felipe Monteirof2b58d72017-08-31 22:40:36 +0100286 ``expected_error_code`` and a message explaining that a non-Forbidden
Felipe Monteiro973a1bc2017-06-14 21:23:54 +0100287 exception was expected, if applicable.
288 """
Rick Bartra12998942017-03-17 17:35:45 -0400289 expected_exception = None
290 irregular_msg = None
Felipe Monteiro973a1bc2017-06-14 21:23:54 +0100291
292 if not isinstance(expected_error_code, six.integer_types) \
Sean Pryor7f8993f2017-08-14 12:53:17 -0400293 or expected_error_code not in _SUPPORTED_ERROR_CODES:
Felipe Monteiro973a1bc2017-06-14 21:23:54 +0100294 msg = ("Please pass an expected error code. Currently "
295 "supported codes: {0}".format(_SUPPORTED_ERROR_CODES))
296 LOG.error(msg)
297 raise rbac_exceptions.RbacInvalidErrorCode(msg)
Felipe Monteiro78fc4892017-04-12 21:33:39 +0100298
Rick Bartra12998942017-03-17 17:35:45 -0400299 if expected_error_code == 403:
300 expected_exception = exceptions.Forbidden
301 elif expected_error_code == 404:
302 expected_exception = exceptions.NotFound
303 irregular_msg = ("NotFound exception was caught for policy action "
304 "{0}. The service {1} throws a 404 instead of a 403, "
305 "which is irregular.")
Rick Bartra12998942017-03-17 17:35:45 -0400306
307 return expected_exception, irregular_msg
Felipe Monteirofd1db982017-04-13 21:19:41 +0100308
309
310def _format_extra_target_data(test_obj, extra_target_data):
311 """Formats the "extra_target_data" dictionary with correct test data.
312
313 Before being formatted, "extra_target_data" is a dictionary that maps a
Felipe Monteiro01d633b2017-08-16 20:17:26 +0100314 policy string like "trust.trustor_user_id" to a nested list of
Felipe Monteirof2b58d72017-08-31 22:40:36 +0100315 ``tempest.test.BaseTestCase`` attributes. For example, the attribute list
Masayuki Igawa80b9aab2018-01-09 17:00:45 +0900316 in::
Felipe Monteirofd1db982017-04-13 21:19:41 +0100317
Masayuki Igawa80b9aab2018-01-09 17:00:45 +0900318 "trust.trustor_user_id": "os.auth_provider.credentials.user_id"
Felipe Monteirofd1db982017-04-13 21:19:41 +0100319
Felipe Monteiro01d633b2017-08-16 20:17:26 +0100320 is parsed by iteratively calling ``getattr`` until the value of "user_id"
Masayuki Igawa80b9aab2018-01-09 17:00:45 +0900321 is resolved. The resulting dictionary returns::
Felipe Monteirofd1db982017-04-13 21:19:41 +0100322
Masayuki Igawa80b9aab2018-01-09 17:00:45 +0900323 "trust.trustor_user_id": "the user_id of the `os_primary` credential"
Felipe Monteirofd1db982017-04-13 21:19:41 +0100324
Felipe Monteirof2b58d72017-08-31 22:40:36 +0100325 :param test_obj: An instance or subclass of ``tempest.test.BaseTestCase``.
326 :param extra_target_data: Dictionary, keyed with ``oslo.policy`` generic
Felipe Monteiro01d633b2017-08-16 20:17:26 +0100327 check names, whose values are string literals that reference nested
Felipe Monteirof2b58d72017-08-31 22:40:36 +0100328 ``tempest.test.BaseTestCase`` attributes. Used by ``oslo.policy`` for
Felipe Monteiro01d633b2017-08-16 20:17:26 +0100329 performing matching against attributes that are sent along with the API
330 calls.
331 :returns: Dictionary containing additional object data needed by
Felipe Monteirof2b58d72017-08-31 22:40:36 +0100332 ``oslo.policy`` to validate generic checks.
Felipe Monteirofd1db982017-04-13 21:19:41 +0100333 """
334 attr_value = test_obj
335 formatted_target_data = {}
336
337 for user_attribute, attr_string in extra_target_data.items():
338 attrs = attr_string.split('.')
339 for attr in attrs:
340 attr_value = getattr(attr_value, attr)
341 formatted_target_data[user_attribute] = attr_value
342
343 return formatted_target_data