blob: d77b2d65ba68b489887901d2a8ae00417084d5de [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
DavidPurcell029d8c32017-01-06 15:27:41 -050028
29CONF = config.CONF
30LOG = logging.getLogger(__name__)
31
32
Felipe Monteiroe7e552e2017-05-02 17:04:12 +010033def action(service, rule='', admin_only=False, expected_error_code=403,
Felipe Monteiro0854ded2017-05-05 16:30:55 +010034 extra_target_data=None):
Felipe Monteirod5d76b82017-03-20 23:18:50 +000035 """A decorator which does a policy check and matches it against test run.
36
37 A decorator which allows for positive and negative RBAC testing. Given
38 an OpenStack service and a policy action enforced by that service, an
39 oslo.policy lookup is performed by calling `authority.get_permission`.
40 The following cases are possible:
41
42 * If `allowed` is True and the test passes, this is a success.
43 * If `allowed` is True and the test fails, this is a failure.
44 * If `allowed` is False and the test passes, this is a failure.
45 * If `allowed` is False and the test fails, this is a success.
46
47 :param service: A OpenStack service: for example, "nova" or "neutron".
48 :param rule: A policy action defined in a policy.json file (or in code).
49 :param admin_only: Skips over oslo.policy check because the policy action
50 defined by `rule` is not enforced by the service's
51 policy enforcement logic. For example, Keystone v2
52 performs an admin check for most of its endpoints. If
53 True, `rule` is effectively ignored.
54 :param expected_error_code: Overrides default value of 403 (Forbidden)
55 with endpoint-specific error code. Currently
56 only supports 403 and 404. Support for 404
57 is needed because some services, like Neutron,
58 intentionally throw a 404 for security reasons.
59
60 :raises NotFound: if `service` is invalid or
61 if Tempest credentials cannot be found.
62 :raises Forbidden: for bullet (2) above.
63 :raises RbacOverPermission: for bullet (3) above.
64 """
Felipe Monteiro0854ded2017-05-05 16:30:55 +010065
66 if extra_target_data is None:
67 extra_target_data = {}
68
DavidPurcell029d8c32017-01-06 15:27:41 -050069 def decorator(func):
Felipe Monteiro78fc4892017-04-12 21:33:39 +010070 role = CONF.rbac.rbac_test_role
71
DavidPurcell029d8c32017-01-06 15:27:41 -050072 def wrapper(*args, **kwargs):
Felipe Monteiro78fc4892017-04-12 21:33:39 +010073 if args and isinstance(args[0], test.BaseTestCase):
74 test_obj = args[0]
75 else:
76 raise rbac_exceptions.RbacResourceSetupFailed(
77 '`rbac_rule_validation` decorator can only be applied to '
78 'an instance of `tempest.test.BaseTestCase`.')
raiesmh088590c0c2017-03-14 18:06:52 +053079
Felipe Monteirod5d76b82017-03-20 23:18:50 +000080 if admin_only:
81 LOG.info("As admin_only is True, only admin role should be "
82 "allowed to perform the API. Skipping oslo.policy "
83 "check for policy action {0}.".format(rule))
Felipe Monteirof6b69e22017-05-04 21:55:04 +010084 allowed = CONF.rbac.rbac_test_role == CONF.identity.admin_role
Felipe Monteirod5d76b82017-03-20 23:18:50 +000085 else:
Felipe Monteiro78fc4892017-04-12 21:33:39 +010086 allowed = _is_authorized(test_obj, service, rule,
87 extra_target_data)
Felipe Monteirod5d76b82017-03-20 23:18:50 +000088
Rick Bartra12998942017-03-17 17:35:45 -040089 expected_exception, irregular_msg = _get_exception_type(
90 expected_error_code)
DavidPurcell029d8c32017-01-06 15:27:41 -050091
92 try:
Felipe Monteiro78fc4892017-04-12 21:33:39 +010093 func(*args, **kwargs)
Rick Bartra503c5572017-03-09 13:49:58 -050094 except rbac_exceptions.RbacInvalidService as e:
Felipe Monteiro48c913d2017-03-15 12:07:48 -040095 msg = ("%s is not a valid service." % service)
96 LOG.error(msg)
97 raise exceptions.NotFound(
Felipe Monteiro78fc4892017-04-12 21:33:39 +010098 "%s RbacInvalidService was: %s" % (msg, e))
Felipe Monteiro8eda8cc2017-03-22 14:15:14 +000099 except (expected_exception, rbac_exceptions.RbacActionFailed) as e:
100 if irregular_msg:
101 LOG.warning(irregular_msg.format(rule, service))
DavidPurcell029d8c32017-01-06 15:27:41 -0500102 if allowed:
103 msg = ("Role %s was not allowed to perform %s." %
Felipe Monteiro78fc4892017-04-12 21:33:39 +0100104 (role, rule))
DavidPurcell029d8c32017-01-06 15:27:41 -0500105 LOG.error(msg)
106 raise exceptions.Forbidden(
Felipe Monteiro78fc4892017-04-12 21:33:39 +0100107 "%s exception was: %s" % (msg, e))
Felipe Monteiro8eda8cc2017-03-22 14:15:14 +0000108 except Exception as e:
109 exc_info = sys.exc_info()
110 error_details = exc_info[1].__str__()
111 msg = ("%s An unexpected exception has occurred: Expected "
112 "exception was %s, which was not thrown."
113 % (error_details, expected_exception.__name__))
114 LOG.error(msg)
115 six.reraise(exc_info[0], exc_info[0](msg), exc_info[2])
DavidPurcell029d8c32017-01-06 15:27:41 -0500116 else:
117 if not allowed:
118 LOG.error("Role %s was allowed to perform %s" %
Felipe Monteiro78fc4892017-04-12 21:33:39 +0100119 (role, rule))
DavidPurcell029d8c32017-01-06 15:27:41 -0500120 raise rbac_exceptions.RbacOverPermission(
121 "OverPermission: Role %s was allowed to perform %s" %
Felipe Monteiro78fc4892017-04-12 21:33:39 +0100122 (role, rule))
raiesmh088590c0c2017-03-14 18:06:52 +0530123 finally:
Felipe Monteiro78fc4892017-04-12 21:33:39 +0100124 test_obj.rbac_utils.switch_role(test_obj,
125 toggle_rbac_role=False)
126
127 _wrapper = testtools.testcase.attr(role)(wrapper)
128 return _wrapper
DavidPurcell029d8c32017-01-06 15:27:41 -0500129 return decorator
Rick Bartra12998942017-03-17 17:35:45 -0400130
131
Felipe Monteiro78fc4892017-04-12 21:33:39 +0100132def _is_authorized(test_obj, service, rule_name, extra_target_data):
133 try:
134 project_id = test_obj.auth_provider.credentials.project_id
135 user_id = test_obj.auth_provider.credentials.user_id
136 except AttributeError as e:
137 msg = ("{0}: project_id/user_id not found in "
138 "cls.auth_provider.credentials".format(e))
139 LOG.error(msg)
140 raise rbac_exceptions.RbacResourceSetupFailed(msg)
141
142 try:
143 role = CONF.rbac.rbac_test_role
144 formatted_target_data = _format_extra_target_data(
145 test_obj, extra_target_data)
146 policy_parser = rbac_policy_parser.RbacPolicyParser(
147 project_id, user_id, service,
148 extra_target_data=formatted_target_data)
149 is_allowed = policy_parser.allowed(rule_name, role)
150
151 if is_allowed:
152 LOG.debug("[Action]: %s, [Role]: %s is allowed!", rule_name,
153 role)
154 else:
155 LOG.debug("[Action]: %s, [Role]: %s is NOT allowed!",
156 rule_name, role)
157 return is_allowed
158 except rbac_exceptions.RbacParsingException as e:
159 if CONF.rbac.strict_policy_check:
160 raise e
161 else:
162 raise testtools.TestCase.skipException(str(e))
163 return False
164
165
Rick Bartra12998942017-03-17 17:35:45 -0400166def _get_exception_type(expected_error_code):
167 expected_exception = None
168 irregular_msg = None
169 supported_error_codes = [403, 404]
Felipe Monteiro78fc4892017-04-12 21:33:39 +0100170
Rick Bartra12998942017-03-17 17:35:45 -0400171 if expected_error_code == 403:
172 expected_exception = exceptions.Forbidden
173 elif expected_error_code == 404:
174 expected_exception = exceptions.NotFound
175 irregular_msg = ("NotFound exception was caught for policy action "
176 "{0}. The service {1} throws a 404 instead of a 403, "
177 "which is irregular.")
178 else:
179 msg = ("Please pass an expected error code. Currently "
180 "supported codes: {0}".format(str(supported_error_codes)))
181 LOG.error(msg)
182 raise rbac_exceptions.RbacInvalidErrorCode()
183
184 return expected_exception, irregular_msg
Felipe Monteirofd1db982017-04-13 21:19:41 +0100185
186
187def _format_extra_target_data(test_obj, extra_target_data):
188 """Formats the "extra_target_data" dictionary with correct test data.
189
190 Before being formatted, "extra_target_data" is a dictionary that maps a
191 policy string like "trust.trustor_user_id" to a nested list of BaseTestCase
192 attributes. For example, the attribute list in:
193
194 "trust.trustor_user_id": "os.auth_provider.credentials.user_id"
195
196 is parsed by iteratively calling `getattr` until the value of "user_id"
197 is resolved. The resulting dictionary returns:
198
199 "trust.trustor_user_id": "the user_id of the `primary` credential"
200
201 :param test_obj: type BaseTestCase (tempest base test class)
202 :param extra_target_data: dictionary with unresolved string literals that
203 reference nested BaseTestCase attributes
204 :returns: dictionary with resolved BaseTestCase attributes
205 """
206 attr_value = test_obj
207 formatted_target_data = {}
208
209 for user_attribute, attr_string in extra_target_data.items():
210 attrs = attr_string.split('.')
211 for attr in attrs:
212 attr_value = getattr(attr_value, attr)
213 formatted_target_data[user_attribute] = attr_value
214
215 return formatted_target_data