blob: aa8bb6a290012d61d85e81f396aab0f0f26b7834 [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
Michael Johnson1eb2c4f2022-03-23 23:30:24 +000036 def _get_client_project_id(self, cred_obj, client_str):
37 """Get project ID for the credential."""
38 dns_clients = getattr(cred_obj, 'dns_v2')
39 client = getattr(dns_clients, client_str)
40 client_obj = client()
41 return client_obj.project_id
42
Michael Johnsonbf2379b2021-08-27 00:04:50 +000043 def _check_allowed(self, client_str, method_str, allowed_list,
Michael Johnson1eb2c4f2022-03-23 23:30:24 +000044 with_project, *args, **kwargs):
Michael Johnsonbf2379b2021-08-27 00:04:50 +000045 """Test an API call allowed RBAC enforcement.
46
47 :param client_str: The service client to use for the test, without the
48 credential. Example: 'ZonesClient'
49 :param method_str: The method on the client to call for the test.
50 Example: 'list_zones'
51 :param allowed_list: The list of credentials expected to be
52 allowed. Example: ['primary'].
Michael Johnson1eb2c4f2022-03-23 23:30:24 +000053 :param with_project: When true, pass the project ID to the call.
Michael Johnsonbf2379b2021-08-27 00:04:50 +000054 :param args: Any positional parameters needed by the method.
55 :param kwargs: Any named parameters needed by the method.
56 :raises AssertionError: Raised if the RBAC tests fail.
57 :raises Forbidden: Raised if a credential that should have access does
58 not and is denied.
59 :raises InvalidScope: Raised if a credential that should have the
60 correct scope for access is denied.
61 :returns: None on success
62 """
63 for cred in allowed_list:
64 try:
65 cred_obj = getattr(self, cred)
66 except AttributeError:
67 # TODO(johnsom) Remove once scoped tokens is the default.
68 if ((cred == 'os_system_admin' or
69 cred == 'os_system_reader') and
70 not CONF.enforce_scope.designate):
71 LOG.info('Skipping %s allowed RBAC test because '
72 'enforce_scope.designate is not True', cred)
73 continue
74 else:
75 self.fail('Credential {} "expected_allowed" for RBAC '
76 'testing was not created by tempest '
77 'credentials setup. This is likely a bug in the '
78 'test.'.format(cred))
79 method = self._get_client_method(cred_obj, client_str, method_str)
Michael Johnson1eb2c4f2022-03-23 23:30:24 +000080 project_id = self._get_client_project_id(cred_obj, client_str)
Michael Johnsonbf2379b2021-08-27 00:04:50 +000081 try:
Michael Johnson1eb2c4f2022-03-23 23:30:24 +000082 if with_project:
83 method(project_id, *args, **kwargs)
84 else:
85 method(*args, **kwargs)
Michael Johnsonbf2379b2021-08-27 00:04:50 +000086 except exceptions.Forbidden as e:
87 self.fail('Method {}.{} failed to allow access via RBAC using '
88 'credential {}. Error: {}'.format(
89 client_str, method_str, cred, str(e)))
Michael Johnson3ff84af2021-09-03 20:16:34 +000090 except exceptions.NotFound as e:
91 self.fail('Method {}.{} failed to allow access via RBAC using '
92 'credential {}. Error: {}'.format(
93 client_str, method_str, cred, str(e)))
Michael Johnsonbf2379b2021-08-27 00:04:50 +000094
95 def _check_disallowed(self, client_str, method_str, allowed_list,
Michael Johnson1eb2c4f2022-03-23 23:30:24 +000096 expect_404, with_project, *args, **kwargs):
Michael Johnsonbf2379b2021-08-27 00:04:50 +000097 """Test an API call disallowed RBAC enforcement.
98
99 :param client_str: The service client to use for the test, without the
100 credential. Example: 'ZonesClient'
101 :param method_str: The method on the client to call for the test.
102 Example: 'list_zones'
103 :param allowed_list: The list of credentials expected to be
104 allowed. Example: ['primary'].
Michael Johnson3ff84af2021-09-03 20:16:34 +0000105 :param expect_404: When True, 404 responses are considered ok.
Michael Johnson1eb2c4f2022-03-23 23:30:24 +0000106 :param with_project: When true, pass the project ID to the call.
Michael Johnsonbf2379b2021-08-27 00:04:50 +0000107 :param args: Any positional parameters needed by the method.
108 :param kwargs: Any named parameters needed by the method.
109 :raises AssertionError: Raised if the RBAC tests fail.
110 :raises Forbidden: Raised if a credential that should have access does
111 not and is denied.
112 :raises InvalidScope: Raised if a credential that should have the
113 correct scope for access is denied.
114 :returns: None on success
115 """
116 expected_disallowed = (set(self.allocated_credentials) -
117 set(allowed_list))
118 for cred in expected_disallowed:
119 cred_obj = getattr(self, cred)
120 method = self._get_client_method(cred_obj, client_str, method_str)
Michael Johnson1eb2c4f2022-03-23 23:30:24 +0000121 project_id = self._get_client_project_id(cred_obj, client_str)
Michael Johnsonbf2379b2021-08-27 00:04:50 +0000122
123 # Unfortunately tempest uses testtools assertRaises[1] which means
124 # we cannot use the unittest assertRaises context[2] with msg= to
125 # give a useful error.
126 # Also, testtools doesn't work with subTest[3], so we can't use
127 # that to expose the failing credential.
128 # This all means the exception raised testtools assertRaises
129 # is less than useful.
130 # TODO(johnsom) Remove this try block once testtools is useful.
131 # [1] https://testtools.readthedocs.io/en/latest/
132 # api.html#testtools.TestCase.assertRaises
133 # [2] https://docs.python.org/3/library/
134 # unittest.html#unittest.TestCase.assertRaises
135 # [3] https://github.com/testing-cabal/testtools/issues/235
136 try:
Michael Johnson1eb2c4f2022-03-23 23:30:24 +0000137 if with_project:
138 method(project_id, *args, **kwargs)
139 else:
140 method(*args, **kwargs)
Michael Johnsonbf2379b2021-08-27 00:04:50 +0000141 except exceptions.Forbidden:
142 continue
Michael Johnson3ff84af2021-09-03 20:16:34 +0000143 except exceptions.NotFound:
144 # Some APIs hide that the resource exists by returning 404
145 # on permission denied.
146 if expect_404:
147 continue
148 raise
Michael Johnsonbf2379b2021-08-27 00:04:50 +0000149 self.fail('Method {}.{} failed to deny access via RBAC using '
150 'credential {}.'.format(client_str, method_str, cred))
151
152 def check_list_show_RBAC_enforcement(self, client_str, method_str,
Michael Johnson3ff84af2021-09-03 20:16:34 +0000153 expected_allowed, expect_404,
154 *args, **kwargs):
Michael Johnsonbf2379b2021-08-27 00:04:50 +0000155 """Test list or show API call RBAC enforcement.
156
157 :param client_str: The service client to use for the test, without the
158 credential. Example: 'ZonesClient'
159 :param method_str: The method on the client to call for the test.
160 Example: 'list_zones'
161 :param expected_allowed: The list of credentials expected to be
162 allowed. Example: ['primary'].
Michael Johnson3ff84af2021-09-03 20:16:34 +0000163 :param expect_404: When True, 404 responses are considered ok.
Michael Johnsonbf2379b2021-08-27 00:04:50 +0000164 :param args: Any positional parameters needed by the method.
165 :param kwargs: Any named parameters needed by the method.
166 :raises AssertionError: Raised if the RBAC tests fail.
167 :raises Forbidden: Raised if a credential that should have access does
168 not and is denied.
169 :raises InvalidScope: Raised if a credential that should have the
170 correct scope for access is denied.
171 :returns: None on success
172 """
173
174 allowed_list = copy.deepcopy(expected_allowed)
175
176 # #### Test that disallowed credentials cannot access the API.
177 self._check_disallowed(client_str, method_str, allowed_list,
Michael Johnson1eb2c4f2022-03-23 23:30:24 +0000178 expect_404, False, *args, **kwargs)
Michael Johnsonbf2379b2021-08-27 00:04:50 +0000179
180 # #### Test that allowed credentials can access the API.
Michael Johnson1eb2c4f2022-03-23 23:30:24 +0000181 self._check_allowed(client_str, method_str, allowed_list, False,
Michael Johnsonbf2379b2021-08-27 00:04:50 +0000182 *args, **kwargs)
183
Michael Johnson1eb2c4f2022-03-23 23:30:24 +0000184 def check_list_show_with_ID_RBAC_enforcement(self, client_str, method_str,
185 expected_allowed, expect_404,
186 *args, **kwargs):
187 """Test list or show API call passing the project ID RBAC enforcement.
Michael Johnsonbf2379b2021-08-27 00:04:50 +0000188
189 :param client_str: The service client to use for the test, without the
190 credential. Example: 'ZonesClient'
191 :param method_str: The method on the client to call for the test.
192 Example: 'list_zones'
193 :param expected_allowed: The list of credentials expected to be
194 allowed. Example: ['primary'].
Michael Johnson1eb2c4f2022-03-23 23:30:24 +0000195 :param expect_404: When True, 404 responses are considered ok.
Michael Johnsonbf2379b2021-08-27 00:04:50 +0000196 :param args: Any positional parameters needed by the method.
197 :param kwargs: Any named parameters needed by the method.
198 :raises AssertionError: Raised if the RBAC tests fail.
199 :raises Forbidden: Raised if a credential that should have access does
200 not and is denied.
201 :raises InvalidScope: Raised if a credential that should have the
202 correct scope for access is denied.
203 :returns: None on success
204 """
205
206 allowed_list = copy.deepcopy(expected_allowed)
207
208 # #### Test that disallowed credentials cannot access the API.
209 self._check_disallowed(client_str, method_str, allowed_list,
Michael Johnson1eb2c4f2022-03-23 23:30:24 +0000210 expect_404, True, *args, **kwargs)
211
212 # #### Test that allowed credentials can access the API.
213 self._check_allowed(client_str, method_str, allowed_list, True,
214 *args, **kwargs)
215
216 def check_CUD_RBAC_enforcement(self, client_str, method_str,
217 expected_allowed, expect_404,
218 *args, **kwargs):
219 """Test an API create/update/delete call RBAC enforcement.
220
221 :param client_str: The service client to use for the test, without the
222 credential. Example: 'ZonesClient'
223 :param method_str: The method on the client to call for the test.
224 Example: 'list_zones'
225 :param expected_allowed: The list of credentials expected to be
226 allowed. Example: ['primary'].
227 :param expect_404: When True, 404 responses are considered ok.
228 :param args: Any positional parameters needed by the method.
229 :param kwargs: Any named parameters needed by the method.
230 :raises AssertionError: Raised if the RBAC tests fail.
231 :raises Forbidden: Raised if a credential that should have access does
232 not and is denied.
233 :raises InvalidScope: Raised if a credential that should have the
234 correct scope for access is denied.
235 :returns: None on success
236 """
237
238 allowed_list = copy.deepcopy(expected_allowed)
239
240 # #### Test that disallowed credentials cannot access the API.
241 self._check_disallowed(client_str, method_str, allowed_list,
242 expect_404, False, *args, **kwargs)
Michael Johnsonbf2379b2021-08-27 00:04:50 +0000243
244 def check_list_RBAC_enforcement_count(
245 self, client_str, method_str, expected_allowed, expected_count,
246 *args, **kwargs):
247 """Test an API list call RBAC enforcement result count.
248
249 List APIs will return the object list for the project associated
250 with the token used to access the API. This means most credentials
251 will have access, but will get differing results.
252
253 This test will query the list API using a list of credentials and
254 will validate that only the expected count of results are returned.
255
256 :param client_str: The service client to use for the test, without the
257 credential. Example: 'ZonesClient'
258 :param method_str: The method on the client to call for the test.
259 Example: 'list_zones'
260 :param expected_allowed: The list of credentials expected to be
261 allowed. Example: ['primary'].
262 :param expected_count: The number of results expected in the list
263 returned from the API.
264 :param args: Any positional parameters needed by the method.
265 :param kwargs: Any named parameters needed by the method.
266 :raises AssertionError: Raised if the RBAC tests fail.
267 :raises Forbidden: Raised if a credential that should have access does
268 not and is denied.
269 :raises InvalidScope: Raised if a credential that should have the
270 correct scope for access is denied.
271 :returns: None on success
272 """
273
274 allowed_list = copy.deepcopy(expected_allowed)
275
276 for cred in allowed_list:
277 try:
278 cred_obj = getattr(self, cred)
279 except AttributeError:
280 # TODO(johnsom) Remove once scoped tokens is the default.
281 if ((cred == 'os_system_admin' or
282 cred == 'os_system_reader') and
283 not CONF.enforce_scope.designate):
284 LOG.info('Skipping %s allowed RBAC test because '
285 'enforce_scope.designate is not True', cred)
286 continue
287 else:
288 self.fail('Credential {} "expected_allowed" for RBAC '
289 'testing was not created by tempest '
290 'credentials setup. This is likely a bug in the '
291 'test.'.format(cred))
292 method = self._get_client_method(cred_obj, client_str, method_str)
293 try:
294 # Get the result body
295 result = method(*args, **kwargs)[1]
296 except exceptions.Forbidden as e:
297 self.fail('Method {}.{} failed to allow access via RBAC using '
298 'credential {}. Error: {}'.format(
299 client_str, method_str, cred, str(e)))
300 # Remove the root element
301 result_objs = next(iter(result.values()))
302
303 self.assertEqual(expected_count, len(result_objs),
304 message='Credential {} saw {} objects when {} '
Michael Johnsona12691b2022-09-09 23:59:56 +0000305 'was expected.'.format(cred, len(result_objs),
Michael Johnsonbf2379b2021-08-27 00:04:50 +0000306 expected_count))
307
308 def check_list_IDs_RBAC_enforcement(
309 self, client_str, method_str, expected_allowed, expected_ids,
310 *args, **kwargs):
311 """Test an API list call RBAC enforcement result contains IDs.
312
313 List APIs will return the object list for the project associated
314 with the token used to access the API. This means most credentials
315 will have access, but will get differing results.
316
317 This test will query the list API using a list of credentials and
318 will validate that the expected object Ids in included in the results.
319
320 :param client_str: The service client to use for the test, without the
321 credential. Example: 'ZonesClient'
322 :param method_str: The method on the client to call for the test.
323 Example: 'list_zones'
324 :param expected_allowed: The list of credentials expected to be
325 allowed. Example: ['primary'].
326 :param expected_ids: The list of object IDs to validate are included
327 in the returned list from the API.
328 :param args: Any positional parameters needed by the method.
329 :param kwargs: Any named parameters needed by the method.
330 :raises AssertionError: Raised if the RBAC tests fail.
331 :raises Forbidden: Raised if a credential that should have access does
332 not and is denied.
333 :raises InvalidScope: Raised if a credential that should have the
334 correct scope for access is denied.
335 :returns: None on success
336 """
337
338 allowed_list = copy.deepcopy(expected_allowed)
339
340 for cred in allowed_list:
341 try:
342 cred_obj = getattr(self, cred)
343 except AttributeError:
344 # TODO(johnsom) Remove once scoped tokens is the default.
345 if ((cred == 'os_system_admin' or
346 cred == 'os_system_reader') and
347 not CONF.enforce_scope.designate):
348 LOG.info('Skipping %s allowed RBAC test because '
349 'enforce_scope.designate is not True', cred)
350 continue
351 else:
352 self.fail('Credential {} "expected_allowed" for RBAC '
353 'testing was not created by tempest '
354 'credentials setup. This is likely a bug in the '
355 'test.'.format(cred))
356 method = self._get_client_method(cred_obj, client_str, method_str)
357 try:
358 # Get the result body
359 result = method(*args, **kwargs)[1]
360 except exceptions.Forbidden as e:
361 self.fail('Method {}.{} failed to allow access via RBAC using '
362 'credential {}. Error: {}'.format(
363 client_str, method_str, cred, str(e)))
364 # Remove the root element
365 result_objs = next(iter(result.values()))
366
367 result_ids = [result_obj["id"] for result_obj in result_objs]
368 self.assertTrue(set(expected_ids).issubset(set(result_ids)))