blob: a225c7d7caec5870f980e62fb49875b8a712d136 [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
18
19import six
Felipe Monteirob0595652017-01-23 16:51:58 -050020
DavidPurcell029d8c32017-01-06 15:27:41 -050021from tempest import config
22from tempest.lib import exceptions
raiesmh088590c0c2017-03-14 18:06:52 +053023from tempest import test
DavidPurcell029d8c32017-01-06 15:27:41 -050024
25from patrole_tempest_plugin import rbac_auth
26from patrole_tempest_plugin import rbac_exceptions
27
28CONF = config.CONF
29LOG = logging.getLogger(__name__)
30
31
Felipe Monteiroe7e552e2017-05-02 17:04:12 +010032def action(service, rule='', admin_only=False, expected_error_code=403,
Felipe Monteirofd1db982017-04-13 21:19:41 +010033 extra_target_data={}):
Felipe Monteirod5d76b82017-03-20 23:18:50 +000034 """A decorator which does a policy check and matches it against test run.
35
36 A decorator which allows for positive and negative RBAC testing. Given
37 an OpenStack service and a policy action enforced by that service, an
38 oslo.policy lookup is performed by calling `authority.get_permission`.
39 The following cases are possible:
40
41 * If `allowed` is True and the test passes, this is a success.
42 * If `allowed` is True and the test fails, this is a failure.
43 * If `allowed` is False and the test passes, this is a failure.
44 * If `allowed` is False and the test fails, this is a success.
45
46 :param service: A OpenStack service: for example, "nova" or "neutron".
47 :param rule: A policy action defined in a policy.json file (or in code).
48 :param admin_only: Skips over oslo.policy check because the policy action
49 defined by `rule` is not enforced by the service's
50 policy enforcement logic. For example, Keystone v2
51 performs an admin check for most of its endpoints. If
52 True, `rule` is effectively ignored.
53 :param expected_error_code: Overrides default value of 403 (Forbidden)
54 with endpoint-specific error code. Currently
55 only supports 403 and 404. Support for 404
56 is needed because some services, like Neutron,
57 intentionally throw a 404 for security reasons.
58
59 :raises NotFound: if `service` is invalid or
60 if Tempest credentials cannot be found.
61 :raises Forbidden: for bullet (2) above.
62 :raises RbacOverPermission: for bullet (3) above.
63 """
DavidPurcell029d8c32017-01-06 15:27:41 -050064 def decorator(func):
65 def wrapper(*args, **kwargs):
Felipe Monteirocbd06172017-01-24 13:49:16 -050066 try:
raiesmh088590c0c2017-03-14 18:06:52 +053067 caller_ref = None
68 if args and isinstance(args[0], test.BaseTestCase):
69 caller_ref = args[0]
Felipe Monteirofd1db982017-04-13 21:19:41 +010070 project_id = caller_ref.auth_provider.credentials.project_id
raiesmh088590c0c2017-03-14 18:06:52 +053071 user_id = caller_ref.auth_provider.credentials.user_id
72 except AttributeError as e:
Felipe Monteirofd1db982017-04-13 21:19:41 +010073 msg = ("{0}: project_id/user_id not found in "
Felipe Monteirocbd06172017-01-24 13:49:16 -050074 "cls.auth_provider.credentials".format(e))
75 LOG.error(msg)
76 raise rbac_exceptions.RbacResourceSetupFailed(msg)
raiesmh088590c0c2017-03-14 18:06:52 +053077
Felipe Monteirod5d76b82017-03-20 23:18:50 +000078 if admin_only:
79 LOG.info("As admin_only is True, only admin role should be "
80 "allowed to perform the API. Skipping oslo.policy "
81 "check for policy action {0}.".format(rule))
82 allowed = CONF.rbac.rbac_test_role == 'admin'
83 else:
Felipe Monteirofd1db982017-04-13 21:19:41 +010084 authority = rbac_auth.RbacAuthority(
85 project_id, user_id, service,
86 _format_extra_target_data(caller_ref, extra_target_data))
87 allowed = authority.get_permission(
88 rule, CONF.rbac.rbac_test_role)
Felipe Monteirod5d76b82017-03-20 23:18:50 +000089
Rick Bartra12998942017-03-17 17:35:45 -040090 expected_exception, irregular_msg = _get_exception_type(
91 expected_error_code)
DavidPurcell029d8c32017-01-06 15:27:41 -050092
93 try:
94 func(*args)
Rick Bartra503c5572017-03-09 13:49:58 -050095 except rbac_exceptions.RbacInvalidService as e:
Felipe Monteiro48c913d2017-03-15 12:07:48 -040096 msg = ("%s is not a valid service." % service)
97 LOG.error(msg)
98 raise exceptions.NotFound(
99 "%s RbacInvalidService was: %s" %
100 (msg, e))
Felipe Monteiro8eda8cc2017-03-22 14:15:14 +0000101 except (expected_exception, rbac_exceptions.RbacActionFailed) as e:
102 if irregular_msg:
103 LOG.warning(irregular_msg.format(rule, service))
DavidPurcell029d8c32017-01-06 15:27:41 -0500104 if allowed:
105 msg = ("Role %s was not allowed to perform %s." %
106 (CONF.rbac.rbac_test_role, rule))
107 LOG.error(msg)
108 raise exceptions.Forbidden(
109 "%s exception was: %s" %
110 (msg, e))
Felipe Monteiro8eda8cc2017-03-22 14:15:14 +0000111 except Exception as e:
112 exc_info = sys.exc_info()
113 error_details = exc_info[1].__str__()
114 msg = ("%s An unexpected exception has occurred: Expected "
115 "exception was %s, which was not thrown."
116 % (error_details, expected_exception.__name__))
117 LOG.error(msg)
118 six.reraise(exc_info[0], exc_info[0](msg), exc_info[2])
DavidPurcell029d8c32017-01-06 15:27:41 -0500119 else:
120 if not allowed:
121 LOG.error("Role %s was allowed to perform %s" %
122 (CONF.rbac.rbac_test_role, rule))
123 raise rbac_exceptions.RbacOverPermission(
124 "OverPermission: Role %s was allowed to perform %s" %
125 (CONF.rbac.rbac_test_role, rule))
raiesmh088590c0c2017-03-14 18:06:52 +0530126 finally:
127 caller_ref.rbac_utils.switch_role(caller_ref,
Felipe Monteiro75f23632017-04-07 15:56:26 +0100128 toggle_rbac_role=False)
DavidPurcell029d8c32017-01-06 15:27:41 -0500129 return wrapper
130 return decorator
Rick Bartra12998942017-03-17 17:35:45 -0400131
132
133def _get_exception_type(expected_error_code):
134 expected_exception = None
135 irregular_msg = None
136 supported_error_codes = [403, 404]
137 if expected_error_code == 403:
138 expected_exception = exceptions.Forbidden
139 elif expected_error_code == 404:
140 expected_exception = exceptions.NotFound
141 irregular_msg = ("NotFound exception was caught for policy action "
142 "{0}. The service {1} throws a 404 instead of a 403, "
143 "which is irregular.")
144 else:
145 msg = ("Please pass an expected error code. Currently "
146 "supported codes: {0}".format(str(supported_error_codes)))
147 LOG.error(msg)
148 raise rbac_exceptions.RbacInvalidErrorCode()
149
150 return expected_exception, irregular_msg
Felipe Monteirofd1db982017-04-13 21:19:41 +0100151
152
153def _format_extra_target_data(test_obj, extra_target_data):
154 """Formats the "extra_target_data" dictionary with correct test data.
155
156 Before being formatted, "extra_target_data" is a dictionary that maps a
157 policy string like "trust.trustor_user_id" to a nested list of BaseTestCase
158 attributes. For example, the attribute list in:
159
160 "trust.trustor_user_id": "os.auth_provider.credentials.user_id"
161
162 is parsed by iteratively calling `getattr` until the value of "user_id"
163 is resolved. The resulting dictionary returns:
164
165 "trust.trustor_user_id": "the user_id of the `primary` credential"
166
167 :param test_obj: type BaseTestCase (tempest base test class)
168 :param extra_target_data: dictionary with unresolved string literals that
169 reference nested BaseTestCase attributes
170 :returns: dictionary with resolved BaseTestCase attributes
171 """
172 attr_value = test_obj
173 formatted_target_data = {}
174
175 for user_attribute, attr_string in extra_target_data.items():
176 attrs = attr_string.split('.')
177 for attr in attrs:
178 attr_value = getattr(attr_value, attr)
179 formatted_target_data[user_attribute] = attr_value
180
181 return formatted_target_data