blob: 6aab4d7271bfae247ee6ea03691cf2dc80c3218b [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
Sergey Vilgelmbab9e942018-10-11 14:04:48 -050016import contextlib
Mykola Yakovliev11376ab2018-08-06 15:34:22 -050017import sys
DavidPurcell029d8c32017-01-06 15:27:41 -050018import time
Felipe Monteiro34a138c2017-03-02 17:01:37 -050019
Rajiv Kumar645dfc92017-01-19 13:48:27 +053020from oslo_log import log as logging
Felipe Monteiro2693bf72017-08-12 22:56:47 +010021from oslo_utils import excutils
Felipe Monteirofa01d5f2017-04-01 06:18:25 +010022
Felipe Monteiro3e14f472017-08-17 23:02:11 +010023from tempest import clients
Felipe Monteiroe7e552e2017-05-02 17:04:12 +010024from tempest.common import credentials_factory as credentials
Felipe Monteirofa01d5f2017-04-01 06:18:25 +010025from tempest import config
Felipe Monteirobf524fb2018-10-03 09:03:35 -050026from tempest.lib import exceptions as lib_exc
DavidPurcell029d8c32017-01-06 15:27:41 -050027
Felipe Monteiro34a138c2017-03-02 17:01:37 -050028from patrole_tempest_plugin import rbac_exceptions
DavidPurcell029d8c32017-01-06 15:27:41 -050029
DavidPurcell029d8c32017-01-06 15:27:41 -050030CONF = config.CONF
Felipe Monteiro34a138c2017-03-02 17:01:37 -050031LOG = logging.getLogger(__name__)
DavidPurcell029d8c32017-01-06 15:27:41 -050032
33
Sergey Vilgelmbab9e942018-10-11 14:04:48 -050034class _ValidateListContext(object):
35 """Context class responsible for validation of the list functions.
36
37 This class is used in ``override_role_and_validate_list`` function and
38 the result of a list function must be assigned to the ``ctx.resources``
39 variable.
40
41 Example::
42
43 with self.rbac_utils.override_role_and_validate_list(...) as ctx:
44 ctx.resources = list_function()
45
46 """
47 def __init__(self, admin_resources=None, admin_resource_id=None):
48 """Constructor for ``ValidateListContext``.
49
50 Either ``admin_resources`` or ``admin_resource_id`` should be used,
51 not both.
52
53 :param list admin_resources: The list of resources received before
54 calling the ``override_role_and_validate_list`` function. To
55 validate will be used the ``_validate_len`` function.
56 :param UUID admin_resource_id: An ID of a resource created before
57 calling the ``override_role_and_validate_list`` function. To
58 validate will be used the ``_validate_resource`` function.
59 :raises RbacValidateListException: if both ``admin_resources`` and
60 ``admin_resource_id`` are set or unset.
61 """
62 self.resources = None
63 if admin_resources is not None and not admin_resource_id:
64 self._admin_len = len(admin_resources)
65 if not self._admin_len:
66 raise rbac_exceptions.RbacValidateListException(
67 reason="the list of admin resources cannot be empty")
68 self._validate_func = self._validate_len
69 elif admin_resource_id and admin_resources is None:
70 self._admin_resource_id = admin_resource_id
71 self._validate_func = self._validate_resource
72 else:
73 raise rbac_exceptions.RbacValidateListException(
74 reason="admin_resources and admin_resource_id are mutually "
75 "exclusive")
76
77 def _validate_len(self):
78 """Validates that the number of resources is less than admin resources.
79 """
80 if not len(self.resources):
81 raise rbac_exceptions.RbacEmptyResponseBody()
82 elif self._admin_len > len(self.resources):
83 raise rbac_exceptions.RbacPartialResponseBody(body=self.resources)
84
85 def _validate_resource(self):
86 """Validates that the admin resource is present in the resources.
87 """
88 for resource in self.resources:
89 if resource['id'] == self._admin_resource_id:
90 return
91 raise rbac_exceptions.RbacPartialResponseBody(body=self.resources)
92
93 def _validate(self):
94 """Calls the proper validation function.
95
96 :raises RbacValidateListException: if the ``ctx.resources`` variable is
97 not assigned.
98 """
99 if self.resources is None:
100 raise rbac_exceptions.RbacValidateListException(
101 reason="ctx.resources is not assigned")
102 self._validate_func()
103
104
DavidPurcell029d8c32017-01-06 15:27:41 -0500105class RbacUtils(object):
Felipe Monteiro10e82fd2017-11-21 01:47:20 +0000106 """Utility class responsible for switching ``os_primary`` role.
Felipe Monteiroe8d93e02017-07-19 20:52:20 +0100107
108 This class is responsible for overriding the value of the primary Tempest
Felipe Monteiro10e82fd2017-11-21 01:47:20 +0000109 credential's role (i.e. ``os_primary`` role). By doing so, it is possible
110 to seamlessly swap between admin credentials, needed for setup and clean
111 up, and primary credentials, needed to perform the API call which does
Felipe Monteiroe8d93e02017-07-19 20:52:20 +0100112 policy enforcement. The primary credentials always cycle between roles
Felipe Monteiro10e82fd2017-11-21 01:47:20 +0000113 defined by ``CONF.identity.admin_role`` and
Mykola Yakovlieve0f35502018-09-26 18:26:57 -0500114 ``CONF.patrole.rbac_test_roles``.
Felipe Monteiroe8d93e02017-07-19 20:52:20 +0100115 """
DavidPurcell029d8c32017-01-06 15:27:41 -0500116
Felipe Monteirob35de582017-05-05 00:16:53 +0100117 def __init__(self, test_obj):
Felipe Monteiroe8d93e02017-07-19 20:52:20 +0100118 """Constructor for ``RbacUtils``.
119
Felipe Monteiro2693bf72017-08-12 22:56:47 +0100120 :param test_obj: An instance of `tempest.test.BaseTestCase`.
Felipe Monteiroe8d93e02017-07-19 20:52:20 +0100121 """
Felipe Monteiroe8d93e02017-07-19 20:52:20 +0100122 # Intialize the admin roles_client to perform role switching.
Felipe Monteiro3e14f472017-08-17 23:02:11 +0100123 admin_mgr = clients.Manager(
124 credentials.get_configured_admin_credentials())
Felipe Monteirobf524fb2018-10-03 09:03:35 -0500125 if CONF.identity_feature_enabled.api_v3:
Felipe Monteiro3e14f472017-08-17 23:02:11 +0100126 admin_roles_client = admin_mgr.roles_v3_client
Felipe Monteiroe8d93e02017-07-19 20:52:20 +0100127 else:
Felipe Monteirobf524fb2018-10-03 09:03:35 -0500128 raise lib_exc.InvalidConfiguration(
129 "Patrole role overriding only supports v3 identity API.")
Felipe Monteiroe8d93e02017-07-19 20:52:20 +0100130
131 self.admin_roles_client = admin_roles_client
Mykola Yakovlieve0f35502018-09-26 18:26:57 -0500132
133 self.user_id = test_obj.os_primary.credentials.user_id
134 self.project_id = test_obj.os_primary.credentials.tenant_id
135
136 # Change default role to admin
Felipe Monteiro10e82fd2017-11-21 01:47:20 +0000137 self._override_role(test_obj, False)
Felipe Monteirob35de582017-05-05 00:16:53 +0100138
Felipe Monteirofa01d5f2017-04-01 06:18:25 +0100139 admin_role_id = None
Mykola Yakovlieve0f35502018-09-26 18:26:57 -0500140 rbac_role_ids = None
DavidPurcell029d8c32017-01-06 15:27:41 -0500141
Sergey Vilgelmbab9e942018-10-11 14:04:48 -0500142 @contextlib.contextmanager
Felipe Monteiro10e82fd2017-11-21 01:47:20 +0000143 def override_role(self, test_obj):
144 """Override the role used by ``os_primary`` Tempest credentials.
145
146 Temporarily change the role used by ``os_primary`` credentials to:
Masayuki Igawa80b9aab2018-01-09 17:00:45 +0900147
Mykola Yakovlieve0f35502018-09-26 18:26:57 -0500148 * ``[patrole] rbac_test_roles`` before test execution
Masayuki Igawa80b9aab2018-01-09 17:00:45 +0900149 * ``[identity] admin_role`` after test execution
Felipe Monteiro10e82fd2017-11-21 01:47:20 +0000150
151 Automatically switches to admin role after test execution.
152
153 :param test_obj: Instance of ``tempest.test.BaseTestCase``.
154 :returns: None
155
156 .. warning::
157
158 This function can alter user roles for pre-provisioned credentials.
159 Work is underway to safely clean up after this function.
160
161 Example::
162
163 @rbac_rule_validation.action(service='test',
Felipe Monteiro59f538f2018-08-22 23:34:40 -0400164 rules=['a:test:rule'])
Felipe Monteiro10e82fd2017-11-21 01:47:20 +0000165 def test_foo(self):
166 # Allocate test-level resources here.
167 with self.rbac_utils.override_role(self):
melissaml7cd21612018-05-23 21:00:50 +0800168 # The role for `os_primary` has now been overridden. Within
Felipe Monteiro10e82fd2017-11-21 01:47:20 +0000169 # this block, call the API endpoint that enforces the
170 # expected policy specified by "rule" in the decorator.
171 self.foo_service.bar_api_call()
172 # The role is switched back to admin automatically. Note that
173 # if the API call above threw an exception, any code below this
174 # point in the test is not executed.
175 """
Mykola Yakovliev11376ab2018-08-06 15:34:22 -0500176 test_obj._set_override_role_called()
Felipe Monteiro10e82fd2017-11-21 01:47:20 +0000177 self._override_role(test_obj, True)
178 try:
179 # Execute the test.
180 yield
181 finally:
Mykola Yakovliev11376ab2018-08-06 15:34:22 -0500182 # Check whether an exception was raised. If so, remember that
183 # for future validation.
184 exc = sys.exc_info()[0]
185 if exc is not None:
186 test_obj._set_override_role_caught_exc()
Felipe Monteiro10e82fd2017-11-21 01:47:20 +0000187 # This code block is always executed, no matter the result of the
188 # test. Automatically switch back to the admin role for test clean
189 # up.
190 self._override_role(test_obj, False)
191
Felipe Monteiro10e82fd2017-11-21 01:47:20 +0000192 def _override_role(self, test_obj, toggle_rbac_role=False):
193 """Private helper for overriding ``os_primary`` Tempest credentials.
194
Felipe Monteiro07a1c172017-12-10 04:26:08 +0000195 :param test_obj: instance of :py:class:`tempest.test.BaseTestCase`
Felipe Monteiro10e82fd2017-11-21 01:47:20 +0000196 :param toggle_rbac_role: Boolean value that controls the role that
197 overrides default role of ``os_primary`` credentials.
198 * If True: role is set to ``[patrole] rbac_test_role``
199 * If False: role is set to ``[identity] admin_role``
200 """
Felipe Monteiro10e82fd2017-11-21 01:47:20 +0000201 LOG.debug('Overriding role to: %s.', toggle_rbac_role)
Mykola Yakovlieve0f35502018-09-26 18:26:57 -0500202 roles_already_present = False
Felipe Monteiro2693bf72017-08-12 22:56:47 +0100203
DavidPurcell029d8c32017-01-06 15:27:41 -0500204 try:
Mykola Yakovlieve0f35502018-09-26 18:26:57 -0500205 if not all([self.admin_role_id, self.rbac_role_ids]):
Felipe Monteiro2693bf72017-08-12 22:56:47 +0100206 self._get_roles_by_name()
DavidPurcell029d8c32017-01-06 15:27:41 -0500207
Mykola Yakovlieve0f35502018-09-26 18:26:57 -0500208 target_roles = (self.rbac_role_ids
209 if toggle_rbac_role else [self.admin_role_id])
210 roles_already_present = self._list_and_clear_user_roles_on_project(
211 target_roles)
Felipe Monteiro2693bf72017-08-12 22:56:47 +0100212
Felipe Monteiro10e82fd2017-11-21 01:47:20 +0000213 # Do not override roles if `target_role` already exists.
Mykola Yakovlieve0f35502018-09-26 18:26:57 -0500214 if not roles_already_present:
215 self._create_user_role_on_project(target_roles)
DavidPurcell029d8c32017-01-06 15:27:41 -0500216 except Exception as exp:
Felipe Monteiro2693bf72017-08-12 22:56:47 +0100217 with excutils.save_and_reraise_exception():
218 LOG.exception(exp)
Felipe Monteiro34a138c2017-03-02 17:01:37 -0500219 finally:
Mykola Yakovliev1d829782018-08-03 14:37:37 -0500220 auth_providers = test_obj.get_auth_providers()
221 for provider in auth_providers:
222 provider.clear_auth()
Felipe Monteiro7be94e82017-07-26 02:17:08 +0100223 # Fernet tokens are not subsecond aware so sleep to ensure we are
224 # passing the second boundary before attempting to authenticate.
Felipe Monteiro2693bf72017-08-12 22:56:47 +0100225 # Only sleep if a token revocation occurred as a result of role
Felipe Monteiro10e82fd2017-11-21 01:47:20 +0000226 # overriding. This will optimize test runtime in the case where
Mykola Yakovlieve0f35502018-09-26 18:26:57 -0500227 # ``[identity] admin_role`` == ``[patrole] rbac_test_roles``.
228 if not roles_already_present:
Rick Bartra89f498f2017-03-20 15:54:45 -0400229 time.sleep(1)
Mykola Yakovliev1d829782018-08-03 14:37:37 -0500230
231 for provider in auth_providers:
232 provider.set_auth()
Felipe Monteiro34a138c2017-03-02 17:01:37 -0500233
Felipe Monteiro2693bf72017-08-12 22:56:47 +0100234 def _get_roles_by_name(self):
Felipe Monteiro2fc29292018-06-15 18:26:27 -0400235 available_roles = self.admin_roles_client.list_roles()['roles']
236 role_map = {r['name']: r['id'] for r in available_roles}
237 LOG.debug('Available roles: %s', list(role_map.keys()))
Felipe Monteirofa01d5f2017-04-01 06:18:25 +0100238
Mykola Yakovlieve0f35502018-09-26 18:26:57 -0500239 rbac_role_ids = []
240 roles = CONF.patrole.rbac_test_roles
241 # TODO(vegasq) drop once CONF.patrole.rbac_test_role is removed
242 if CONF.patrole.rbac_test_role:
243 if not roles:
244 roles.append(CONF.patrole.rbac_test_role)
Felipe Monteiro2693bf72017-08-12 22:56:47 +0100245
Mykola Yakovlieve0f35502018-09-26 18:26:57 -0500246 for role_name in roles:
247 rbac_role_ids.append(role_map.get(role_name))
248
249 admin_role_id = role_map.get(CONF.identity.admin_role)
250
251 if not all([admin_role_id, all(rbac_role_ids)]):
Felipe Monteiro2fc29292018-06-15 18:26:27 -0400252 missing_roles = []
Mykola Yakovlieve0f35502018-09-26 18:26:57 -0500253 msg = ("Could not find `[patrole] rbac_test_roles` or "
Felipe Monteiro2fc29292018-06-15 18:26:27 -0400254 "`[identity] admin_role`, both of which are required for "
255 "RBAC testing.")
256 if not admin_role_id:
257 missing_roles.append(CONF.identity.admin_role)
Mykola Yakovlieve0f35502018-09-26 18:26:57 -0500258 if not all(rbac_role_ids):
259 missing_roles += [role_name for role_name in roles
260 if not role_map.get(role_name)]
261
Felipe Monteiro2fc29292018-06-15 18:26:27 -0400262 msg += " Following roles were not found: %s." % (
263 ", ".join(missing_roles))
264 msg += " Available roles: %s." % ", ".join(list(role_map.keys()))
Felipe Monteiro2693bf72017-08-12 22:56:47 +0100265 raise rbac_exceptions.RbacResourceSetupFailed(msg)
266
267 self.admin_role_id = admin_role_id
Mykola Yakovlieve0f35502018-09-26 18:26:57 -0500268 self.rbac_role_ids = rbac_role_ids
Felipe Monteiro2693bf72017-08-12 22:56:47 +0100269
Mykola Yakovlieve0f35502018-09-26 18:26:57 -0500270 def _create_user_role_on_project(self, role_ids):
271 for role_id in role_ids:
272 self.admin_roles_client.create_user_role_on_project(
273 self.project_id, self.user_id, role_id)
Felipe Monteirofa01d5f2017-04-01 06:18:25 +0100274
Mykola Yakovlieve0f35502018-09-26 18:26:57 -0500275 def _list_and_clear_user_roles_on_project(self, role_ids):
Felipe Monteiroe8d93e02017-07-19 20:52:20 +0100276 roles = self.admin_roles_client.list_user_roles_on_project(
Felipe Monteirofa01d5f2017-04-01 06:18:25 +0100277 self.project_id, self.user_id)['roles']
Mykola Yakovlieve0f35502018-09-26 18:26:57 -0500278 all_role_ids = [role['id'] for role in roles]
Felipe Monteiro2693bf72017-08-12 22:56:47 +0100279
Mykola Yakovlieve0f35502018-09-26 18:26:57 -0500280 # NOTE(felipemonteiro): We do not use ``role_id in all_role_ids`` here
281 # to avoid over-permission errors: if the current list of roles on the
Felipe Monteiro2693bf72017-08-12 22:56:47 +0100282 # project includes "admin" and "Member", and we are switching to the
283 # "Member" role, then we must delete the "admin" role. Thus, we only
284 # return early if the user's roles on the project are an exact match.
Mykola Yakovlieve0f35502018-09-26 18:26:57 -0500285 if set(role_ids) == set(all_role_ids):
Felipe Monteirofa01d5f2017-04-01 06:18:25 +0100286 return True
Felipe Monteirob3b7bc82017-03-03 15:58:15 -0500287
288 for role in roles:
Felipe Monteiroe8d93e02017-07-19 20:52:20 +0100289 self.admin_roles_client.delete_role_from_user_on_project(
Felipe Monteirofa01d5f2017-04-01 06:18:25 +0100290 self.project_id, self.user_id, role['id'])
291
292 return False
293
Sergey Vilgelmbab9e942018-10-11 14:04:48 -0500294 @contextlib.contextmanager
295 def override_role_and_validate_list(self, test_obj, admin_resources=None,
296 admin_resource_id=None):
297 """Call ``override_role`` and validate RBAC for a list API action.
298
299 List actions usually do soft authorization: partial or empty response
300 bodies are returned instead of exceptions. This helper validates
301 that unauthorized roles only return a subset of the available
302 resources.
303 Should only be used for validating list API actions.
304
305 :param test_obj: Instance of ``tempest.test.BaseTestCase``.
306 :param list admin_resources: The list of resources received before
307 calling the ``override_role_and_validate_list`` function.
308 :param UUID admin_resource_id: An ID of a resource created before
309 calling the ``override_role_and_validate_list`` function.
310 :return: py:class:`_ValidateListContext` object.
311
312 Example::
313
314 # the resource created by admin
315 admin_resource_id = (
316 self.ntp_client.create_dscp_marking_rule()
317 ["dscp_marking_rule"]["id'])
318 with self.rbac_utils.override_role_and_validate_list(
319 self, admin_resource_id=admin_resource_id) as ctx:
320 # the list of resources available for member role
321 ctx.resources = self.ntp_client.list_dscp_marking_rules(
322 policy_id=self.policy_id)["dscp_marking_rules"]
323 """
324 ctx = _ValidateListContext(admin_resources, admin_resource_id)
325 with self.override_role(test_obj):
326 yield ctx
327 ctx._validate()
328
Felipe Monteiro17e9b492017-05-27 05:45:20 +0100329
Felipe Monteiro07a1c172017-12-10 04:26:08 +0000330class RbacUtilsMixin(object):
331 """Mixin class to be used alongside an instance of
332 :py:class:`tempest.test.BaseTestCase`.
333
334 Should be used to perform Patrole class setup for a base RBAC class. Child
335 classes should not use this mixin.
336
337 Example::
338
339 class BaseRbacTest(rbac_utils.RbacUtilsMixin, base.BaseV2ComputeTest):
340
341 @classmethod
342 def skip_checks(cls):
343 super(BaseRbacTest, cls).skip_checks()
344 cls.skip_rbac_checks()
345
346 @classmethod
347 def setup_clients(cls):
348 super(BaseRbacTest, cls).setup_clients()
349 cls.setup_rbac_utils()
350 """
351
Mykola Yakovliev11376ab2018-08-06 15:34:22 -0500352 # Shows if override_role was called.
353 __override_role_called = False
354 # Shows if exception raised during override_role.
355 __override_role_caught_exc = False
356
Felipe Monteiro07a1c172017-12-10 04:26:08 +0000357 @classmethod
Mykola Yakovliev1d829782018-08-03 14:37:37 -0500358 def get_auth_providers(cls):
359 """Returns list of auth_providers used within test.
360
361 Tests may redefine this method to include their own or third party
362 client auth_providers.
363 """
364 return [cls.os_primary.auth_provider]
365
366 @classmethod
Felipe Monteiro07a1c172017-12-10 04:26:08 +0000367 def setup_rbac_utils(cls):
368 cls.rbac_utils = RbacUtils(cls)
369
Mykola Yakovliev11376ab2018-08-06 15:34:22 -0500370 def _set_override_role_called(self):
371 """Helper for tracking whether ``override_role`` was called."""
372 self.__override_role_called = True
373
374 def _set_override_role_caught_exc(self):
375 """Helper for tracking whether exception was thrown inside
376 ``override_role``.
377 """
378 self.__override_role_caught_exc = True
379
380 def _validate_override_role_called(self):
381 """Idempotently validate that ``override_role`` is called and reset
382 its value to False for sequential tests.
383 """
384 was_called = self.__override_role_called
385 self.__override_role_called = False
386 return was_called
387
388 def _validate_override_role_caught_exc(self):
389 """Idempotently validate that exception was caught inside
390 ``override_role``, so that, by process of elimination, it can be
391 determined whether one was thrown outside (which is invalid).
392 """
393 caught_exception = self.__override_role_caught_exc
394 self.__override_role_caught_exc = False
395 return caught_exception
396
Felipe Monteiro07a1c172017-12-10 04:26:08 +0000397
Felipe Monteiro8a043fb2017-08-06 06:29:05 +0100398def is_admin():
399 """Verifies whether the current test role equals the admin role.
400
Mykola Yakovlieve0f35502018-09-26 18:26:57 -0500401 :returns: True if ``rbac_test_roles`` contain the admin role.
Felipe Monteiro8a043fb2017-08-06 06:29:05 +0100402 """
Mykola Yakovlieve0f35502018-09-26 18:26:57 -0500403 roles = CONF.patrole.rbac_test_roles
404 # TODO(vegasq) drop once CONF.patrole.rbac_test_role is removed
405 if CONF.patrole.rbac_test_role:
406 roles.append(CONF.patrole.rbac_test_role)
407 roles = list(set(roles))
408
Felipe Monteiro2fc29292018-06-15 18:26:27 -0400409 # TODO(felipemonteiro): Make this more robust via a context is admin
410 # lookup.
Mykola Yakovlieve0f35502018-09-26 18:26:57 -0500411 return CONF.identity.admin_role in roles