blob: e02fdf1bd98feae55ddefb3dd93aa9a1dde277e8 [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)))
78
79 def _check_disallowed(self, client_str, method_str, allowed_list,
80 *args, **kwargs):
81 """Test an API call disallowed RBAC enforcement.
82
83 :param client_str: The service client to use for the test, without the
84 credential. Example: 'ZonesClient'
85 :param method_str: The method on the client to call for the test.
86 Example: 'list_zones'
87 :param allowed_list: The list of credentials expected to be
88 allowed. Example: ['primary'].
89 :param args: Any positional parameters needed by the method.
90 :param kwargs: Any named parameters needed by the method.
91 :raises AssertionError: Raised if the RBAC tests fail.
92 :raises Forbidden: Raised if a credential that should have access does
93 not and is denied.
94 :raises InvalidScope: Raised if a credential that should have the
95 correct scope for access is denied.
96 :returns: None on success
97 """
98 expected_disallowed = (set(self.allocated_credentials) -
99 set(allowed_list))
100 for cred in expected_disallowed:
101 cred_obj = getattr(self, cred)
102 method = self._get_client_method(cred_obj, client_str, method_str)
103
104 # Unfortunately tempest uses testtools assertRaises[1] which means
105 # we cannot use the unittest assertRaises context[2] with msg= to
106 # give a useful error.
107 # Also, testtools doesn't work with subTest[3], so we can't use
108 # that to expose the failing credential.
109 # This all means the exception raised testtools assertRaises
110 # is less than useful.
111 # TODO(johnsom) Remove this try block once testtools is useful.
112 # [1] https://testtools.readthedocs.io/en/latest/
113 # api.html#testtools.TestCase.assertRaises
114 # [2] https://docs.python.org/3/library/
115 # unittest.html#unittest.TestCase.assertRaises
116 # [3] https://github.com/testing-cabal/testtools/issues/235
117 try:
118 method(*args, **kwargs)
119 except exceptions.Forbidden:
120 continue
121 self.fail('Method {}.{} failed to deny access via RBAC using '
122 'credential {}.'.format(client_str, method_str, cred))
123
124 def check_list_show_RBAC_enforcement(self, client_str, method_str,
125 expected_allowed, *args, **kwargs):
126 """Test list or show API call RBAC enforcement.
127
128 :param client_str: The service client to use for the test, without the
129 credential. Example: 'ZonesClient'
130 :param method_str: The method on the client to call for the test.
131 Example: 'list_zones'
132 :param expected_allowed: The list of credentials expected to be
133 allowed. Example: ['primary'].
134 :param args: Any positional parameters needed by the method.
135 :param kwargs: Any named parameters needed by the method.
136 :raises AssertionError: Raised if the RBAC tests fail.
137 :raises Forbidden: Raised if a credential that should have access does
138 not and is denied.
139 :raises InvalidScope: Raised if a credential that should have the
140 correct scope for access is denied.
141 :returns: None on success
142 """
143
144 allowed_list = copy.deepcopy(expected_allowed)
145
146 # #### Test that disallowed credentials cannot access the API.
147 self._check_disallowed(client_str, method_str, allowed_list,
148 *args, **kwargs)
149
150 # #### Test that allowed credentials can access the API.
151 self._check_allowed(client_str, method_str, allowed_list,
152 *args, **kwargs)
153
154 def check_CUD_RBAC_enforcement(self, client_str, method_str,
155 expected_allowed, *args, **kwargs):
156 """Test an API create/update/delete call RBAC enforcement.
157
158 :param client_str: The service client to use for the test, without the
159 credential. Example: 'ZonesClient'
160 :param method_str: The method on the client to call for the test.
161 Example: 'list_zones'
162 :param expected_allowed: The list of credentials expected to be
163 allowed. Example: ['primary'].
164 :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,
178 *args, **kwargs)
179
180 def check_list_RBAC_enforcement_count(
181 self, client_str, method_str, expected_allowed, expected_count,
182 *args, **kwargs):
183 """Test an API list call RBAC enforcement result count.
184
185 List APIs will return the object list for the project associated
186 with the token used to access the API. This means most credentials
187 will have access, but will get differing results.
188
189 This test will query the list API using a list of credentials and
190 will validate that only the expected count of results are returned.
191
192 :param client_str: The service client to use for the test, without the
193 credential. Example: 'ZonesClient'
194 :param method_str: The method on the client to call for the test.
195 Example: 'list_zones'
196 :param expected_allowed: The list of credentials expected to be
197 allowed. Example: ['primary'].
198 :param expected_count: The number of results expected in the list
199 returned from the API.
200 :param args: Any positional parameters needed by the method.
201 :param kwargs: Any named parameters needed by the method.
202 :raises AssertionError: Raised if the RBAC tests fail.
203 :raises Forbidden: Raised if a credential that should have access does
204 not and is denied.
205 :raises InvalidScope: Raised if a credential that should have the
206 correct scope for access is denied.
207 :returns: None on success
208 """
209
210 allowed_list = copy.deepcopy(expected_allowed)
211
212 for cred in allowed_list:
213 try:
214 cred_obj = getattr(self, cred)
215 except AttributeError:
216 # TODO(johnsom) Remove once scoped tokens is the default.
217 if ((cred == 'os_system_admin' or
218 cred == 'os_system_reader') and
219 not CONF.enforce_scope.designate):
220 LOG.info('Skipping %s allowed RBAC test because '
221 'enforce_scope.designate is not True', cred)
222 continue
223 else:
224 self.fail('Credential {} "expected_allowed" for RBAC '
225 'testing was not created by tempest '
226 'credentials setup. This is likely a bug in the '
227 'test.'.format(cred))
228 method = self._get_client_method(cred_obj, client_str, method_str)
229 try:
230 # Get the result body
231 result = method(*args, **kwargs)[1]
232 except exceptions.Forbidden as e:
233 self.fail('Method {}.{} failed to allow access via RBAC using '
234 'credential {}. Error: {}'.format(
235 client_str, method_str, cred, str(e)))
236 # Remove the root element
237 result_objs = next(iter(result.values()))
238
239 self.assertEqual(expected_count, len(result_objs),
240 message='Credential {} saw {} objects when {} '
241 'was expected.'.format(cred, len(result),
242 expected_count))
243
244 def check_list_IDs_RBAC_enforcement(
245 self, client_str, method_str, expected_allowed, expected_ids,
246 *args, **kwargs):
247 """Test an API list call RBAC enforcement result contains IDs.
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 the expected object Ids in included in the results.
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_ids: The list of object IDs to validate are included
263 in the returned list 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 result_ids = [result_obj["id"] for result_obj in result_objs]
304 self.assertTrue(set(expected_ids).issubset(set(result_ids)))