blob: 5638ca41534e4dd7981e4eb5b09ec6a5a5138ef7 [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:
Ghanshyam Mannbb1996d2024-08-15 18:52:31 -070067 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))
Michael Johnsonbf2379b2021-08-27 00:04:50 +000071 method = self._get_client_method(cred_obj, client_str, method_str)
Michael Johnson1eb2c4f2022-03-23 23:30:24 +000072 project_id = self._get_client_project_id(cred_obj, client_str)
Michael Johnsonbf2379b2021-08-27 00:04:50 +000073 try:
Michael Johnson1eb2c4f2022-03-23 23:30:24 +000074 if with_project:
75 method(project_id, *args, **kwargs)
76 else:
77 method(*args, **kwargs)
Michael Johnsonbf2379b2021-08-27 00:04:50 +000078 except exceptions.Forbidden 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 Johnson3ff84af2021-09-03 20:16:34 +000082 except exceptions.NotFound as e:
83 self.fail('Method {}.{} failed to allow access via RBAC using '
84 'credential {}. Error: {}'.format(
85 client_str, method_str, cred, str(e)))
Michael Johnsonbf2379b2021-08-27 00:04:50 +000086
87 def _check_disallowed(self, client_str, method_str, allowed_list,
Michael Johnson1eb2c4f2022-03-23 23:30:24 +000088 expect_404, with_project, *args, **kwargs):
Michael Johnsonbf2379b2021-08-27 00:04:50 +000089 """Test an API call disallowed RBAC enforcement.
90
91 :param client_str: The service client to use for the test, without the
92 credential. Example: 'ZonesClient'
93 :param method_str: The method on the client to call for the test.
94 Example: 'list_zones'
95 :param allowed_list: The list of credentials expected to be
96 allowed. Example: ['primary'].
Michael Johnson3ff84af2021-09-03 20:16:34 +000097 :param expect_404: When True, 404 responses are considered ok.
Michael Johnson1eb2c4f2022-03-23 23:30:24 +000098 :param with_project: When true, pass the project ID to the call.
Michael Johnsonbf2379b2021-08-27 00:04:50 +000099 :param args: Any positional parameters needed by the method.
100 :param kwargs: Any named parameters needed by the method.
101 :raises AssertionError: Raised if the RBAC tests fail.
102 :raises Forbidden: Raised if a credential that should have access does
103 not and is denied.
104 :raises InvalidScope: Raised if a credential that should have the
105 correct scope for access is denied.
106 :returns: None on success
107 """
108 expected_disallowed = (set(self.allocated_credentials) -
109 set(allowed_list))
110 for cred in expected_disallowed:
111 cred_obj = getattr(self, cred)
112 method = self._get_client_method(cred_obj, client_str, method_str)
Michael Johnson1eb2c4f2022-03-23 23:30:24 +0000113 project_id = self._get_client_project_id(cred_obj, client_str)
Michael Johnsonbf2379b2021-08-27 00:04:50 +0000114
115 # Unfortunately tempest uses testtools assertRaises[1] which means
116 # we cannot use the unittest assertRaises context[2] with msg= to
117 # give a useful error.
118 # Also, testtools doesn't work with subTest[3], so we can't use
119 # that to expose the failing credential.
120 # This all means the exception raised testtools assertRaises
121 # is less than useful.
122 # TODO(johnsom) Remove this try block once testtools is useful.
123 # [1] https://testtools.readthedocs.io/en/latest/
124 # api.html#testtools.TestCase.assertRaises
125 # [2] https://docs.python.org/3/library/
126 # unittest.html#unittest.TestCase.assertRaises
127 # [3] https://github.com/testing-cabal/testtools/issues/235
128 try:
Michael Johnson1eb2c4f2022-03-23 23:30:24 +0000129 if with_project:
130 method(project_id, *args, **kwargs)
131 else:
132 method(*args, **kwargs)
Michael Johnsonbf2379b2021-08-27 00:04:50 +0000133 except exceptions.Forbidden:
134 continue
Michael Johnson3ff84af2021-09-03 20:16:34 +0000135 except exceptions.NotFound:
136 # Some APIs hide that the resource exists by returning 404
137 # on permission denied.
138 if expect_404:
139 continue
140 raise
Michael Johnsonbf2379b2021-08-27 00:04:50 +0000141 self.fail('Method {}.{} failed to deny access via RBAC using '
142 'credential {}.'.format(client_str, method_str, cred))
143
144 def check_list_show_RBAC_enforcement(self, client_str, method_str,
Michael Johnson3ff84af2021-09-03 20:16:34 +0000145 expected_allowed, expect_404,
146 *args, **kwargs):
Michael Johnsonbf2379b2021-08-27 00:04:50 +0000147 """Test list or show API call RBAC enforcement.
148
149 :param client_str: The service client to use for the test, without the
150 credential. Example: 'ZonesClient'
151 :param method_str: The method on the client to call for the test.
152 Example: 'list_zones'
153 :param expected_allowed: The list of credentials expected to be
154 allowed. Example: ['primary'].
Michael Johnson3ff84af2021-09-03 20:16:34 +0000155 :param expect_404: When True, 404 responses are considered ok.
Michael Johnsonbf2379b2021-08-27 00:04:50 +0000156 :param args: Any positional parameters needed by the method.
157 :param kwargs: Any named parameters needed by the method.
158 :raises AssertionError: Raised if the RBAC tests fail.
159 :raises Forbidden: Raised if a credential that should have access does
160 not and is denied.
161 :raises InvalidScope: Raised if a credential that should have the
162 correct scope for access is denied.
163 :returns: None on success
164 """
165
166 allowed_list = copy.deepcopy(expected_allowed)
167
168 # #### Test that disallowed credentials cannot access the API.
169 self._check_disallowed(client_str, method_str, allowed_list,
Michael Johnson1eb2c4f2022-03-23 23:30:24 +0000170 expect_404, False, *args, **kwargs)
Michael Johnsonbf2379b2021-08-27 00:04:50 +0000171
172 # #### Test that allowed credentials can access the API.
Michael Johnson1eb2c4f2022-03-23 23:30:24 +0000173 self._check_allowed(client_str, method_str, allowed_list, False,
Michael Johnsonbf2379b2021-08-27 00:04:50 +0000174 *args, **kwargs)
175
Michael Johnson1eb2c4f2022-03-23 23:30:24 +0000176 def check_list_show_with_ID_RBAC_enforcement(self, client_str, method_str,
177 expected_allowed, expect_404,
178 *args, **kwargs):
179 """Test list or show API call passing the project ID RBAC enforcement.
Michael Johnsonbf2379b2021-08-27 00:04:50 +0000180
181 :param client_str: The service client to use for the test, without the
182 credential. Example: 'ZonesClient'
183 :param method_str: The method on the client to call for the test.
184 Example: 'list_zones'
185 :param expected_allowed: The list of credentials expected to be
186 allowed. Example: ['primary'].
Michael Johnson1eb2c4f2022-03-23 23:30:24 +0000187 :param expect_404: When True, 404 responses are considered ok.
Michael Johnsonbf2379b2021-08-27 00:04:50 +0000188 :param args: Any positional parameters needed by the method.
189 :param kwargs: Any named parameters needed by the method.
190 :raises AssertionError: Raised if the RBAC tests fail.
191 :raises Forbidden: Raised if a credential that should have access does
192 not and is denied.
193 :raises InvalidScope: Raised if a credential that should have the
194 correct scope for access is denied.
195 :returns: None on success
196 """
197
198 allowed_list = copy.deepcopy(expected_allowed)
199
200 # #### Test that disallowed credentials cannot access the API.
201 self._check_disallowed(client_str, method_str, allowed_list,
Michael Johnson1eb2c4f2022-03-23 23:30:24 +0000202 expect_404, True, *args, **kwargs)
203
204 # #### Test that allowed credentials can access the API.
205 self._check_allowed(client_str, method_str, allowed_list, True,
206 *args, **kwargs)
207
208 def check_CUD_RBAC_enforcement(self, client_str, method_str,
209 expected_allowed, expect_404,
210 *args, **kwargs):
211 """Test an API create/update/delete call RBAC enforcement.
212
213 :param client_str: The service client to use for the test, without the
214 credential. Example: 'ZonesClient'
215 :param method_str: The method on the client to call for the test.
216 Example: 'list_zones'
217 :param expected_allowed: The list of credentials expected to be
218 allowed. Example: ['primary'].
219 :param expect_404: When True, 404 responses are considered ok.
220 :param args: Any positional parameters needed by the method.
221 :param kwargs: Any named parameters needed by the method.
222 :raises AssertionError: Raised if the RBAC tests fail.
223 :raises Forbidden: Raised if a credential that should have access does
224 not and is denied.
225 :raises InvalidScope: Raised if a credential that should have the
226 correct scope for access is denied.
227 :returns: None on success
228 """
229
230 allowed_list = copy.deepcopy(expected_allowed)
231
232 # #### Test that disallowed credentials cannot access the API.
233 self._check_disallowed(client_str, method_str, allowed_list,
234 expect_404, False, *args, **kwargs)
Michael Johnsonbf2379b2021-08-27 00:04:50 +0000235
236 def check_list_RBAC_enforcement_count(
237 self, client_str, method_str, expected_allowed, expected_count,
238 *args, **kwargs):
239 """Test an API list call RBAC enforcement result count.
240
241 List APIs will return the object list for the project associated
242 with the token used to access the API. This means most credentials
243 will have access, but will get differing results.
244
245 This test will query the list API using a list of credentials and
246 will validate that only the expected count of results are returned.
247
248 :param client_str: The service client to use for the test, without the
249 credential. Example: 'ZonesClient'
250 :param method_str: The method on the client to call for the test.
251 Example: 'list_zones'
252 :param expected_allowed: The list of credentials expected to be
253 allowed. Example: ['primary'].
254 :param expected_count: The number of results expected in the list
255 returned from the API.
256 :param args: Any positional parameters needed by the method.
257 :param kwargs: Any named parameters needed by the method.
258 :raises AssertionError: Raised if the RBAC tests fail.
259 :raises Forbidden: Raised if a credential that should have access does
260 not and is denied.
261 :raises InvalidScope: Raised if a credential that should have the
262 correct scope for access is denied.
263 :returns: None on success
264 """
265
266 allowed_list = copy.deepcopy(expected_allowed)
267
268 for cred in allowed_list:
269 try:
270 cred_obj = getattr(self, cred)
271 except AttributeError:
Ghanshyam Mannbb1996d2024-08-15 18:52:31 -0700272 self.fail('Credential {} "expected_allowed" for RBAC '
273 'testing was not created by tempest '
274 'credentials setup. This is likely a bug in the '
275 'test.'.format(cred))
Michael Johnsonbf2379b2021-08-27 00:04:50 +0000276 method = self._get_client_method(cred_obj, client_str, method_str)
277 try:
278 # Get the result body
279 result = method(*args, **kwargs)[1]
280 except exceptions.Forbidden as e:
281 self.fail('Method {}.{} failed to allow access via RBAC using '
282 'credential {}. Error: {}'.format(
283 client_str, method_str, cred, str(e)))
284 # Remove the root element
285 result_objs = next(iter(result.values()))
286
287 self.assertEqual(expected_count, len(result_objs),
288 message='Credential {} saw {} objects when {} '
Michael Johnsona12691b2022-09-09 23:59:56 +0000289 'was expected.'.format(cred, len(result_objs),
Michael Johnsonbf2379b2021-08-27 00:04:50 +0000290 expected_count))
291
292 def check_list_IDs_RBAC_enforcement(
293 self, client_str, method_str, expected_allowed, expected_ids,
294 *args, **kwargs):
295 """Test an API list call RBAC enforcement result contains IDs.
296
297 List APIs will return the object list for the project associated
298 with the token used to access the API. This means most credentials
299 will have access, but will get differing results.
300
301 This test will query the list API using a list of credentials and
302 will validate that the expected object Ids in included in the results.
303
304 :param client_str: The service client to use for the test, without the
305 credential. Example: 'ZonesClient'
306 :param method_str: The method on the client to call for the test.
307 Example: 'list_zones'
308 :param expected_allowed: The list of credentials expected to be
309 allowed. Example: ['primary'].
310 :param expected_ids: The list of object IDs to validate are included
311 in the returned list from the API.
312 :param args: Any positional parameters needed by the method.
313 :param kwargs: Any named parameters needed by the method.
314 :raises AssertionError: Raised if the RBAC tests fail.
315 :raises Forbidden: Raised if a credential that should have access does
316 not and is denied.
317 :raises InvalidScope: Raised if a credential that should have the
318 correct scope for access is denied.
319 :returns: None on success
320 """
321
322 allowed_list = copy.deepcopy(expected_allowed)
323
324 for cred in allowed_list:
325 try:
326 cred_obj = getattr(self, cred)
327 except AttributeError:
Ghanshyam Mannbb1996d2024-08-15 18:52:31 -0700328 self.fail('Credential {} "expected_allowed" for RBAC '
329 'testing was not created by tempest '
330 'credentials setup. This is likely a bug in the '
331 'test.'.format(cred))
Michael Johnsonbf2379b2021-08-27 00:04:50 +0000332 method = self._get_client_method(cred_obj, client_str, method_str)
333 try:
334 # Get the result body
335 result = method(*args, **kwargs)[1]
336 except exceptions.Forbidden as e:
337 self.fail('Method {}.{} failed to allow access via RBAC using '
338 'credential {}. Error: {}'.format(
339 client_str, method_str, cred, str(e)))
340 # Remove the root element
341 result_objs = next(iter(result.values()))
342
343 result_ids = [result_obj["id"] for result_obj in result_objs]
344 self.assertTrue(set(expected_ids).issubset(set(result_ids)))