blob: a0148bb5c1534891fd65ebbcb8b0d98f8918e2a8 [file] [log] [blame]
Michael Johnsonbf2379b2021-08-27 00:04:50 +00001# Copyright 2021 Red Hat, Inc. All rights reserved.
2#
3# Licensed under the Apache License, Version 2.0 (the "License"); you may
4# not use this file except in compliance with the License. You may obtain
5# a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12# License for the specific language governing permissions and limitations
13# under the License.
14
15import copy
16
17from oslo_log import log as logging
18from tempest import config
19from tempest.lib import exceptions
20from tempest import test
21
22CONF = config.CONF
23LOG = logging.getLogger(__name__)
24
25
26class RBACTestsMixin(test.BaseTestCase):
27
28 def _get_client_method(self, cred_obj, client_str, method_str):
29 """Get requested method from registered clients in Tempest."""
30 dns_clients = getattr(cred_obj, 'dns_v2')
31 client = getattr(dns_clients, client_str)
32 client_obj = client()
33 method = getattr(client_obj, method_str)
34 return method
35
36 def _check_allowed(self, client_str, method_str, allowed_list,
37 *args, **kwargs):
38 """Test an API call allowed RBAC enforcement.
39
40 :param client_str: The service client to use for the test, without the
41 credential. Example: 'ZonesClient'
42 :param method_str: The method on the client to call for the test.
43 Example: 'list_zones'
44 :param allowed_list: The list of credentials expected to be
45 allowed. Example: ['primary'].
46 :param args: Any positional parameters needed by the method.
47 :param kwargs: Any named parameters needed by the method.
48 :raises AssertionError: Raised if the RBAC tests fail.
49 :raises Forbidden: Raised if a credential that should have access does
50 not and is denied.
51 :raises InvalidScope: Raised if a credential that should have the
52 correct scope for access is denied.
53 :returns: None on success
54 """
55 for cred in allowed_list:
56 try:
57 cred_obj = getattr(self, cred)
58 except AttributeError:
59 # TODO(johnsom) Remove once scoped tokens is the default.
60 if ((cred == 'os_system_admin' or
61 cred == 'os_system_reader') and
62 not CONF.enforce_scope.designate):
63 LOG.info('Skipping %s allowed RBAC test because '
64 'enforce_scope.designate is not True', cred)
65 continue
66 else:
67 self.fail('Credential {} "expected_allowed" for RBAC '
68 'testing was not created by tempest '
69 'credentials setup. This is likely a bug in the '
70 'test.'.format(cred))
71 method = self._get_client_method(cred_obj, client_str, method_str)
72 try:
73 method(*args, **kwargs)
74 except exceptions.Forbidden as e:
75 self.fail('Method {}.{} failed to allow access via RBAC using '
76 'credential {}. Error: {}'.format(
77 client_str, method_str, cred, str(e)))
Michael Johnson3ff84af2021-09-03 20:16:34 +000078 except exceptions.NotFound as e:
79 self.fail('Method {}.{} failed to allow access via RBAC using '
80 'credential {}. Error: {}'.format(
81 client_str, method_str, cred, str(e)))
Michael Johnsonbf2379b2021-08-27 00:04:50 +000082
83 def _check_disallowed(self, client_str, method_str, allowed_list,
Michael Johnson3ff84af2021-09-03 20:16:34 +000084 expect_404, *args, **kwargs):
Michael Johnsonbf2379b2021-08-27 00:04:50 +000085 """Test an API call disallowed RBAC enforcement.
86
87 :param client_str: The service client to use for the test, without the
88 credential. Example: 'ZonesClient'
89 :param method_str: The method on the client to call for the test.
90 Example: 'list_zones'
91 :param allowed_list: The list of credentials expected to be
92 allowed. Example: ['primary'].
Michael Johnson3ff84af2021-09-03 20:16:34 +000093 :param expect_404: When True, 404 responses are considered ok.
Michael Johnsonbf2379b2021-08-27 00:04:50 +000094 :param args: Any positional parameters needed by the method.
95 :param kwargs: Any named parameters needed by the method.
96 :raises AssertionError: Raised if the RBAC tests fail.
97 :raises Forbidden: Raised if a credential that should have access does
98 not and is denied.
99 :raises InvalidScope: Raised if a credential that should have the
100 correct scope for access is denied.
101 :returns: None on success
102 """
103 expected_disallowed = (set(self.allocated_credentials) -
104 set(allowed_list))
105 for cred in expected_disallowed:
106 cred_obj = getattr(self, cred)
107 method = self._get_client_method(cred_obj, client_str, method_str)
108
109 # Unfortunately tempest uses testtools assertRaises[1] which means
110 # we cannot use the unittest assertRaises context[2] with msg= to
111 # give a useful error.
112 # Also, testtools doesn't work with subTest[3], so we can't use
113 # that to expose the failing credential.
114 # This all means the exception raised testtools assertRaises
115 # is less than useful.
116 # TODO(johnsom) Remove this try block once testtools is useful.
117 # [1] https://testtools.readthedocs.io/en/latest/
118 # api.html#testtools.TestCase.assertRaises
119 # [2] https://docs.python.org/3/library/
120 # unittest.html#unittest.TestCase.assertRaises
121 # [3] https://github.com/testing-cabal/testtools/issues/235
122 try:
123 method(*args, **kwargs)
124 except exceptions.Forbidden:
125 continue
Michael Johnson3ff84af2021-09-03 20:16:34 +0000126 except exceptions.NotFound:
127 # Some APIs hide that the resource exists by returning 404
128 # on permission denied.
129 if expect_404:
130 continue
131 raise
Michael Johnsonbf2379b2021-08-27 00:04:50 +0000132 self.fail('Method {}.{} failed to deny access via RBAC using '
133 'credential {}.'.format(client_str, method_str, cred))
134
135 def check_list_show_RBAC_enforcement(self, client_str, method_str,
Michael Johnson3ff84af2021-09-03 20:16:34 +0000136 expected_allowed, expect_404,
137 *args, **kwargs):
Michael Johnsonbf2379b2021-08-27 00:04:50 +0000138 """Test list or show API call RBAC enforcement.
139
140 :param client_str: The service client to use for the test, without the
141 credential. Example: 'ZonesClient'
142 :param method_str: The method on the client to call for the test.
143 Example: 'list_zones'
144 :param expected_allowed: The list of credentials expected to be
145 allowed. Example: ['primary'].
Michael Johnson3ff84af2021-09-03 20:16:34 +0000146 :param expect_404: When True, 404 responses are considered ok.
Michael Johnsonbf2379b2021-08-27 00:04:50 +0000147 :param args: Any positional parameters needed by the method.
148 :param kwargs: Any named parameters needed by the method.
149 :raises AssertionError: Raised if the RBAC tests fail.
150 :raises Forbidden: Raised if a credential that should have access does
151 not and is denied.
152 :raises InvalidScope: Raised if a credential that should have the
153 correct scope for access is denied.
154 :returns: None on success
155 """
156
157 allowed_list = copy.deepcopy(expected_allowed)
158
159 # #### Test that disallowed credentials cannot access the API.
160 self._check_disallowed(client_str, method_str, allowed_list,
Michael Johnson3ff84af2021-09-03 20:16:34 +0000161 expect_404, *args, **kwargs)
Michael Johnsonbf2379b2021-08-27 00:04:50 +0000162
163 # #### Test that allowed credentials can access the API.
164 self._check_allowed(client_str, method_str, allowed_list,
165 *args, **kwargs)
166
167 def check_CUD_RBAC_enforcement(self, client_str, method_str,
168 expected_allowed, *args, **kwargs):
169 """Test an API create/update/delete call RBAC enforcement.
170
171 :param client_str: The service client to use for the test, without the
172 credential. Example: 'ZonesClient'
173 :param method_str: The method on the client to call for the test.
174 Example: 'list_zones'
175 :param expected_allowed: The list of credentials expected to be
176 allowed. Example: ['primary'].
177 :param args: Any positional parameters needed by the method.
178 :param kwargs: Any named parameters needed by the method.
179 :raises AssertionError: Raised if the RBAC tests fail.
180 :raises Forbidden: Raised if a credential that should have access does
181 not and is denied.
182 :raises InvalidScope: Raised if a credential that should have the
183 correct scope for access is denied.
184 :returns: None on success
185 """
186
187 allowed_list = copy.deepcopy(expected_allowed)
188
189 # #### Test that disallowed credentials cannot access the API.
190 self._check_disallowed(client_str, method_str, allowed_list,
191 *args, **kwargs)
192
193 def check_list_RBAC_enforcement_count(
194 self, client_str, method_str, expected_allowed, expected_count,
195 *args, **kwargs):
196 """Test an API list call RBAC enforcement result count.
197
198 List APIs will return the object list for the project associated
199 with the token used to access the API. This means most credentials
200 will have access, but will get differing results.
201
202 This test will query the list API using a list of credentials and
203 will validate that only the expected count of results are returned.
204
205 :param client_str: The service client to use for the test, without the
206 credential. Example: 'ZonesClient'
207 :param method_str: The method on the client to call for the test.
208 Example: 'list_zones'
209 :param expected_allowed: The list of credentials expected to be
210 allowed. Example: ['primary'].
211 :param expected_count: The number of results expected in the list
212 returned from the API.
213 :param args: Any positional parameters needed by the method.
214 :param kwargs: Any named parameters needed by the method.
215 :raises AssertionError: Raised if the RBAC tests fail.
216 :raises Forbidden: Raised if a credential that should have access does
217 not and is denied.
218 :raises InvalidScope: Raised if a credential that should have the
219 correct scope for access is denied.
220 :returns: None on success
221 """
222
223 allowed_list = copy.deepcopy(expected_allowed)
224
225 for cred in allowed_list:
226 try:
227 cred_obj = getattr(self, cred)
228 except AttributeError:
229 # TODO(johnsom) Remove once scoped tokens is the default.
230 if ((cred == 'os_system_admin' or
231 cred == 'os_system_reader') and
232 not CONF.enforce_scope.designate):
233 LOG.info('Skipping %s allowed RBAC test because '
234 'enforce_scope.designate is not True', cred)
235 continue
236 else:
237 self.fail('Credential {} "expected_allowed" for RBAC '
238 'testing was not created by tempest '
239 'credentials setup. This is likely a bug in the '
240 'test.'.format(cred))
241 method = self._get_client_method(cred_obj, client_str, method_str)
242 try:
243 # Get the result body
244 result = method(*args, **kwargs)[1]
245 except exceptions.Forbidden as e:
246 self.fail('Method {}.{} failed to allow access via RBAC using '
247 'credential {}. Error: {}'.format(
248 client_str, method_str, cred, str(e)))
249 # Remove the root element
250 result_objs = next(iter(result.values()))
251
252 self.assertEqual(expected_count, len(result_objs),
253 message='Credential {} saw {} objects when {} '
254 'was expected.'.format(cred, len(result),
255 expected_count))
256
257 def check_list_IDs_RBAC_enforcement(
258 self, client_str, method_str, expected_allowed, expected_ids,
259 *args, **kwargs):
260 """Test an API list call RBAC enforcement result contains IDs.
261
262 List APIs will return the object list for the project associated
263 with the token used to access the API. This means most credentials
264 will have access, but will get differing results.
265
266 This test will query the list API using a list of credentials and
267 will validate that the expected object Ids in included in the results.
268
269 :param client_str: The service client to use for the test, without the
270 credential. Example: 'ZonesClient'
271 :param method_str: The method on the client to call for the test.
272 Example: 'list_zones'
273 :param expected_allowed: The list of credentials expected to be
274 allowed. Example: ['primary'].
275 :param expected_ids: The list of object IDs to validate are included
276 in the returned list from the API.
277 :param args: Any positional parameters needed by the method.
278 :param kwargs: Any named parameters needed by the method.
279 :raises AssertionError: Raised if the RBAC tests fail.
280 :raises Forbidden: Raised if a credential that should have access does
281 not and is denied.
282 :raises InvalidScope: Raised if a credential that should have the
283 correct scope for access is denied.
284 :returns: None on success
285 """
286
287 allowed_list = copy.deepcopy(expected_allowed)
288
289 for cred in allowed_list:
290 try:
291 cred_obj = getattr(self, cred)
292 except AttributeError:
293 # TODO(johnsom) Remove once scoped tokens is the default.
294 if ((cred == 'os_system_admin' or
295 cred == 'os_system_reader') and
296 not CONF.enforce_scope.designate):
297 LOG.info('Skipping %s allowed RBAC test because '
298 'enforce_scope.designate is not True', cred)
299 continue
300 else:
301 self.fail('Credential {} "expected_allowed" for RBAC '
302 'testing was not created by tempest '
303 'credentials setup. This is likely a bug in the '
304 'test.'.format(cred))
305 method = self._get_client_method(cred_obj, client_str, method_str)
306 try:
307 # Get the result body
308 result = method(*args, **kwargs)[1]
309 except exceptions.Forbidden as e:
310 self.fail('Method {}.{} failed to allow access via RBAC using '
311 'credential {}. Error: {}'.format(
312 client_str, method_str, cred, str(e)))
313 # Remove the root element
314 result_objs = next(iter(result.values()))
315
316 result_ids = [result_obj["id"] for result_obj in result_objs]
317 self.assertTrue(set(expected_ids).issubset(set(result_ids)))