blob: 97053679a9f97e4879bb33b1017cc997725ff66d [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 Monteirod5d76b82017-03-20 23:18:50 +000039 """A decorator which does a policy check and matches it against test run.
40
41 A decorator which allows for positive and negative RBAC testing. Given
42 an OpenStack service and a policy action enforced by that service, an
43 oslo.policy lookup is performed by calling `authority.get_permission`.
Rick Bartraed950052017-06-29 17:20:33 -040044 Alternatively, the RBAC tests can run against a YAML file that defines
45 policy requirements.
46
Felipe Monteirod5d76b82017-03-20 23:18:50 +000047 The following cases are possible:
48
49 * If `allowed` is True and the test passes, this is a success.
50 * If `allowed` is True and the test fails, this is a failure.
51 * If `allowed` is False and the test passes, this is a failure.
52 * If `allowed` is False and the test fails, this is a success.
53
54 :param service: A OpenStack service: for example, "nova" or "neutron".
55 :param rule: A policy action defined in a policy.json file (or in code).
56 :param admin_only: Skips over oslo.policy check because the policy action
Felipe Monteiro973a1bc2017-06-14 21:23:54 +010057 defined by `rule` is not enforced by the service's policy enforcement
58 logic. For example, Keystone v2 performs an admin check for most of its
59 endpoints. If True, `rule` is effectively ignored.
Felipe Monteirod5d76b82017-03-20 23:18:50 +000060 :param expected_error_code: Overrides default value of 403 (Forbidden)
Felipe Monteiro973a1bc2017-06-14 21:23:54 +010061 with endpoint-specific error code. Currently only supports 403 and 404.
62 Support for 404 is needed because some services, like Neutron,
63 intentionally throw a 404 for security reasons.
Felipe Monteirod5d76b82017-03-20 23:18:50 +000064
65 :raises NotFound: if `service` is invalid or
66 if Tempest credentials cannot be found.
67 :raises Forbidden: for bullet (2) above.
68 :raises RbacOverPermission: for bullet (3) above.
69 """
Felipe Monteiro0854ded2017-05-05 16:30:55 +010070
71 if extra_target_data is None:
72 extra_target_data = {}
73
DavidPurcell029d8c32017-01-06 15:27:41 -050074 def decorator(func):
Felipe Monteirof6eb8622017-08-06 06:08:02 +010075 role = CONF.patrole.rbac_test_role
Felipe Monteiro78fc4892017-04-12 21:33:39 +010076
DavidPurcell029d8c32017-01-06 15:27:41 -050077 def wrapper(*args, **kwargs):
Felipe Monteiro78fc4892017-04-12 21:33:39 +010078 if args and isinstance(args[0], test.BaseTestCase):
79 test_obj = args[0]
80 else:
81 raise rbac_exceptions.RbacResourceSetupFailed(
82 '`rbac_rule_validation` decorator can only be applied to '
83 'an instance of `tempest.test.BaseTestCase`.')
raiesmh088590c0c2017-03-14 18:06:52 +053084
Felipe Monteirod5d76b82017-03-20 23:18:50 +000085 if admin_only:
86 LOG.info("As admin_only is True, only admin role should be "
87 "allowed to perform the API. Skipping oslo.policy "
88 "check for policy action {0}.".format(rule))
Felipe Monteiro8a043fb2017-08-06 06:29:05 +010089 allowed = rbac_utils.is_admin()
Felipe Monteirod5d76b82017-03-20 23:18:50 +000090 else:
Felipe Monteiro78fc4892017-04-12 21:33:39 +010091 allowed = _is_authorized(test_obj, service, rule,
92 extra_target_data)
Felipe Monteirod5d76b82017-03-20 23:18:50 +000093
Rick Bartra12998942017-03-17 17:35:45 -040094 expected_exception, irregular_msg = _get_exception_type(
95 expected_error_code)
DavidPurcell029d8c32017-01-06 15:27:41 -050096
97 try:
Felipe Monteiro78fc4892017-04-12 21:33:39 +010098 func(*args, **kwargs)
Rick Bartra503c5572017-03-09 13:49:58 -050099 except rbac_exceptions.RbacInvalidService as e:
Felipe Monteiro48c913d2017-03-15 12:07:48 -0400100 msg = ("%s is not a valid service." % service)
101 LOG.error(msg)
102 raise exceptions.NotFound(
Felipe Monteiro78fc4892017-04-12 21:33:39 +0100103 "%s RbacInvalidService was: %s" % (msg, e))
Samantha Blanco36bea052017-07-19 12:01:59 -0400104 except (expected_exception,
105 rbac_exceptions.RbacConflictingPolicies,
106 rbac_exceptions.RbacMalformedResponse) as e:
Felipe Monteiro8eda8cc2017-03-22 14:15:14 +0000107 if irregular_msg:
108 LOG.warning(irregular_msg.format(rule, service))
DavidPurcell029d8c32017-01-06 15:27:41 -0500109 if allowed:
110 msg = ("Role %s was not allowed to perform %s." %
Felipe Monteiro78fc4892017-04-12 21:33:39 +0100111 (role, rule))
DavidPurcell029d8c32017-01-06 15:27:41 -0500112 LOG.error(msg)
113 raise exceptions.Forbidden(
Felipe Monteiro4bf66a22017-05-07 14:44:21 +0100114 "%s Exception was: %s" % (msg, e))
Felipe Monteiro8eda8cc2017-03-22 14:15:14 +0000115 except Exception as e:
116 exc_info = sys.exc_info()
117 error_details = exc_info[1].__str__()
118 msg = ("%s An unexpected exception has occurred: Expected "
119 "exception was %s, which was not thrown."
120 % (error_details, expected_exception.__name__))
121 LOG.error(msg)
122 six.reraise(exc_info[0], exc_info[0](msg), exc_info[2])
DavidPurcell029d8c32017-01-06 15:27:41 -0500123 else:
124 if not allowed:
Felipe Monteiro4bf66a22017-05-07 14:44:21 +0100125 LOG.error("Role %s was allowed to perform %s",
Felipe Monteiroe52cbc62017-05-24 17:48:59 +0100126 role, rule)
DavidPurcell029d8c32017-01-06 15:27:41 -0500127 raise rbac_exceptions.RbacOverPermission(
128 "OverPermission: Role %s was allowed to perform %s" %
Felipe Monteiro78fc4892017-04-12 21:33:39 +0100129 (role, rule))
raiesmh088590c0c2017-03-14 18:06:52 +0530130 finally:
Felipe Monteiro78fc4892017-04-12 21:33:39 +0100131 test_obj.rbac_utils.switch_role(test_obj,
132 toggle_rbac_role=False)
133
134 _wrapper = testtools.testcase.attr(role)(wrapper)
135 return _wrapper
DavidPurcell029d8c32017-01-06 15:27:41 -0500136 return decorator
Rick Bartra12998942017-03-17 17:35:45 -0400137
138
Felipe Monteiro78fc4892017-04-12 21:33:39 +0100139def _is_authorized(test_obj, service, rule_name, extra_target_data):
Felipe Monteirodea13842017-07-05 04:11:18 +0100140 """Validates whether current RBAC role has permission to do policy action.
141
142 :param test_obj: type BaseTestCase (tempest base test class)
143 :param service: the OpenStack service that enforces ``rule_name``
144 :param rule_name: the name of the policy action
145 :param extra_target_data: dictionary with unresolved string literals that
146 reference nested BaseTestCase attributes
147 :returns: True if the current RBAC role can perform the policy action else
148 False
Felipe Monteiro7be94e82017-07-26 02:17:08 +0100149
150 :raises RbacResourceSetupFailed: if project_id or user_id are missing from
151 the Tempest test object's `auth_provider`
Felipe Monteirof6eb8622017-08-06 06:08:02 +0100152 :raises RbacParsingException: if ``CONF.patrole.strict_policy_check`` is
Felipe Monteirodea13842017-07-05 04:11:18 +0100153 enabled and the ``rule_name`` does not exist in the system
Felipe Monteirof6eb8622017-08-06 06:08:02 +0100154 :raises skipException: if ``CONF.patrole.strict_policy_check`` is
Felipe Monteirodea13842017-07-05 04:11:18 +0100155 disabled and the ``rule_name`` does not exist in the system
156 """
Felipe Monteiro78fc4892017-04-12 21:33:39 +0100157 try:
Felipe Monteiroe8d93e02017-07-19 20:52:20 +0100158 project_id = test_obj.os_primary.credentials.project_id
159 user_id = test_obj.os_primary.credentials.user_id
Felipe Monteiro78fc4892017-04-12 21:33:39 +0100160 except AttributeError as e:
Felipe Monteiroe8d93e02017-07-19 20:52:20 +0100161 msg = ("{0}: project_id or user_id not found in os_primary.credentials"
162 .format(e))
Felipe Monteiro78fc4892017-04-12 21:33:39 +0100163 LOG.error(msg)
164 raise rbac_exceptions.RbacResourceSetupFailed(msg)
165
166 try:
Felipe Monteirof6eb8622017-08-06 06:08:02 +0100167 role = CONF.patrole.rbac_test_role
Rick Bartraed950052017-06-29 17:20:33 -0400168 # Test RBAC against custom requirements. Otherwise use oslo.policy
Felipe Monteirof6eb8622017-08-06 06:08:02 +0100169 if CONF.patrole.test_custom_requirements:
Rick Bartraed950052017-06-29 17:20:33 -0400170 authority = requirements_authority.RequirementsAuthority(
Felipe Monteirof6eb8622017-08-06 06:08:02 +0100171 CONF.patrole.custom_requirements_file, service)
Rick Bartraed950052017-06-29 17:20:33 -0400172 else:
173 formatted_target_data = _format_extra_target_data(
174 test_obj, extra_target_data)
175 authority = rbac_policy_parser.RbacPolicyParser(
176 project_id, user_id, service,
177 extra_target_data=formatted_target_data)
178 is_allowed = authority.allowed(rule_name, role)
Felipe Monteiro78fc4892017-04-12 21:33:39 +0100179
180 if is_allowed:
181 LOG.debug("[Action]: %s, [Role]: %s is allowed!", rule_name,
182 role)
183 else:
184 LOG.debug("[Action]: %s, [Role]: %s is NOT allowed!",
185 rule_name, role)
186 return is_allowed
187 except rbac_exceptions.RbacParsingException as e:
Felipe Monteirof6eb8622017-08-06 06:08:02 +0100188 if CONF.patrole.strict_policy_check:
Felipe Monteiro78fc4892017-04-12 21:33:39 +0100189 raise e
190 else:
191 raise testtools.TestCase.skipException(str(e))
192 return False
193
194
Felipe Monteiro973a1bc2017-06-14 21:23:54 +0100195def _get_exception_type(expected_error_code=403):
196 """Dynamically calculate the expected exception to be caught.
197
198 Dynamically calculate the expected exception to be caught by the test case.
199 Only `Forbidden` and `NotFound` exceptions are permitted. `NotFound` is
200 supported because Neutron, for security reasons, masks `Forbidden`
201 exceptions as `NotFound` exceptions.
202
203 :param expected_error_code: the integer representation of the expected
204 exception to be caught. Must be contained in `_SUPPORTED_ERROR_CODES`.
205 :returns: tuple of the exception type corresponding to
206 `expected_error_code` and a message explaining that a non-Forbidden
207 exception was expected, if applicable.
208 """
Rick Bartra12998942017-03-17 17:35:45 -0400209 expected_exception = None
210 irregular_msg = None
Felipe Monteiro973a1bc2017-06-14 21:23:54 +0100211
212 if not isinstance(expected_error_code, six.integer_types) \
213 or expected_error_code not in _SUPPORTED_ERROR_CODES:
214 msg = ("Please pass an expected error code. Currently "
215 "supported codes: {0}".format(_SUPPORTED_ERROR_CODES))
216 LOG.error(msg)
217 raise rbac_exceptions.RbacInvalidErrorCode(msg)
Felipe Monteiro78fc4892017-04-12 21:33:39 +0100218
Rick Bartra12998942017-03-17 17:35:45 -0400219 if expected_error_code == 403:
220 expected_exception = exceptions.Forbidden
221 elif expected_error_code == 404:
222 expected_exception = exceptions.NotFound
223 irregular_msg = ("NotFound exception was caught for policy action "
224 "{0}. The service {1} throws a 404 instead of a 403, "
225 "which is irregular.")
Rick Bartra12998942017-03-17 17:35:45 -0400226
227 return expected_exception, irregular_msg
Felipe Monteirofd1db982017-04-13 21:19:41 +0100228
229
230def _format_extra_target_data(test_obj, extra_target_data):
231 """Formats the "extra_target_data" dictionary with correct test data.
232
233 Before being formatted, "extra_target_data" is a dictionary that maps a
234 policy string like "trust.trustor_user_id" to a nested list of BaseTestCase
235 attributes. For example, the attribute list in:
236
237 "trust.trustor_user_id": "os.auth_provider.credentials.user_id"
238
239 is parsed by iteratively calling `getattr` until the value of "user_id"
240 is resolved. The resulting dictionary returns:
241
242 "trust.trustor_user_id": "the user_id of the `primary` credential"
243
244 :param test_obj: type BaseTestCase (tempest base test class)
245 :param extra_target_data: dictionary with unresolved string literals that
Felipe Monteiro973a1bc2017-06-14 21:23:54 +0100246 reference nested BaseTestCase attributes
Felipe Monteirodea13842017-07-05 04:11:18 +0100247 :returns: dictionary containing additional object data needed by
248 oslo.policy to validate generic checks
Felipe Monteirofd1db982017-04-13 21:19:41 +0100249 """
250 attr_value = test_obj
251 formatted_target_data = {}
252
253 for user_attribute, attr_string in extra_target_data.items():
254 attrs = attr_string.split('.')
255 for attr in attrs:
256 attr_value = getattr(attr_value, attr)
257 formatted_target_data[user_attribute] = attr_value
258
259 return formatted_target_data