blob: 33955c318a8eac02fabdc98526d1b942d92dd240 [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
Felipe Monteiro10e82fd2017-11-21 01:47:20 +000016from contextlib import contextmanager
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
DavidPurcell029d8c32017-01-06 15:27:41 -050034class RbacUtils(object):
Felipe Monteiro10e82fd2017-11-21 01:47:20 +000035 """Utility class responsible for switching ``os_primary`` role.
Felipe Monteiroe8d93e02017-07-19 20:52:20 +010036
37 This class is responsible for overriding the value of the primary Tempest
Felipe Monteiro10e82fd2017-11-21 01:47:20 +000038 credential's role (i.e. ``os_primary`` role). By doing so, it is possible
39 to seamlessly swap between admin credentials, needed for setup and clean
40 up, and primary credentials, needed to perform the API call which does
Felipe Monteiroe8d93e02017-07-19 20:52:20 +010041 policy enforcement. The primary credentials always cycle between roles
Felipe Monteiro10e82fd2017-11-21 01:47:20 +000042 defined by ``CONF.identity.admin_role`` and
Mykola Yakovlieve0f35502018-09-26 18:26:57 -050043 ``CONF.patrole.rbac_test_roles``.
Felipe Monteiroe8d93e02017-07-19 20:52:20 +010044 """
DavidPurcell029d8c32017-01-06 15:27:41 -050045
Felipe Monteirob35de582017-05-05 00:16:53 +010046 def __init__(self, test_obj):
Felipe Monteiroe8d93e02017-07-19 20:52:20 +010047 """Constructor for ``RbacUtils``.
48
Felipe Monteiro2693bf72017-08-12 22:56:47 +010049 :param test_obj: An instance of `tempest.test.BaseTestCase`.
Felipe Monteiroe8d93e02017-07-19 20:52:20 +010050 """
Felipe Monteiroe8d93e02017-07-19 20:52:20 +010051 # Intialize the admin roles_client to perform role switching.
Felipe Monteiro3e14f472017-08-17 23:02:11 +010052 admin_mgr = clients.Manager(
53 credentials.get_configured_admin_credentials())
Felipe Monteirobf524fb2018-10-03 09:03:35 -050054 if CONF.identity_feature_enabled.api_v3:
Felipe Monteiro3e14f472017-08-17 23:02:11 +010055 admin_roles_client = admin_mgr.roles_v3_client
Felipe Monteiroe8d93e02017-07-19 20:52:20 +010056 else:
Felipe Monteirobf524fb2018-10-03 09:03:35 -050057 raise lib_exc.InvalidConfiguration(
58 "Patrole role overriding only supports v3 identity API.")
Felipe Monteiroe8d93e02017-07-19 20:52:20 +010059
60 self.admin_roles_client = admin_roles_client
Mykola Yakovlieve0f35502018-09-26 18:26:57 -050061
62 self.user_id = test_obj.os_primary.credentials.user_id
63 self.project_id = test_obj.os_primary.credentials.tenant_id
64
65 # Change default role to admin
Felipe Monteiro10e82fd2017-11-21 01:47:20 +000066 self._override_role(test_obj, False)
Felipe Monteirob35de582017-05-05 00:16:53 +010067
Felipe Monteirofa01d5f2017-04-01 06:18:25 +010068 admin_role_id = None
Mykola Yakovlieve0f35502018-09-26 18:26:57 -050069 rbac_role_ids = None
DavidPurcell029d8c32017-01-06 15:27:41 -050070
Felipe Monteiro10e82fd2017-11-21 01:47:20 +000071 @contextmanager
72 def override_role(self, test_obj):
73 """Override the role used by ``os_primary`` Tempest credentials.
74
75 Temporarily change the role used by ``os_primary`` credentials to:
Masayuki Igawa80b9aab2018-01-09 17:00:45 +090076
Mykola Yakovlieve0f35502018-09-26 18:26:57 -050077 * ``[patrole] rbac_test_roles`` before test execution
Masayuki Igawa80b9aab2018-01-09 17:00:45 +090078 * ``[identity] admin_role`` after test execution
Felipe Monteiro10e82fd2017-11-21 01:47:20 +000079
80 Automatically switches to admin role after test execution.
81
82 :param test_obj: Instance of ``tempest.test.BaseTestCase``.
83 :returns: None
84
85 .. warning::
86
87 This function can alter user roles for pre-provisioned credentials.
88 Work is underway to safely clean up after this function.
89
90 Example::
91
92 @rbac_rule_validation.action(service='test',
Felipe Monteiro59f538f2018-08-22 23:34:40 -040093 rules=['a:test:rule'])
Felipe Monteiro10e82fd2017-11-21 01:47:20 +000094 def test_foo(self):
95 # Allocate test-level resources here.
96 with self.rbac_utils.override_role(self):
melissaml7cd21612018-05-23 21:00:50 +080097 # The role for `os_primary` has now been overridden. Within
Felipe Monteiro10e82fd2017-11-21 01:47:20 +000098 # this block, call the API endpoint that enforces the
99 # expected policy specified by "rule" in the decorator.
100 self.foo_service.bar_api_call()
101 # The role is switched back to admin automatically. Note that
102 # if the API call above threw an exception, any code below this
103 # point in the test is not executed.
104 """
Mykola Yakovliev11376ab2018-08-06 15:34:22 -0500105 test_obj._set_override_role_called()
Felipe Monteiro10e82fd2017-11-21 01:47:20 +0000106 self._override_role(test_obj, True)
107 try:
108 # Execute the test.
109 yield
110 finally:
Mykola Yakovliev11376ab2018-08-06 15:34:22 -0500111 # Check whether an exception was raised. If so, remember that
112 # for future validation.
113 exc = sys.exc_info()[0]
114 if exc is not None:
115 test_obj._set_override_role_caught_exc()
Felipe Monteiro10e82fd2017-11-21 01:47:20 +0000116 # This code block is always executed, no matter the result of the
117 # test. Automatically switch back to the admin role for test clean
118 # up.
119 self._override_role(test_obj, False)
120
Felipe Monteiro10e82fd2017-11-21 01:47:20 +0000121 def _override_role(self, test_obj, toggle_rbac_role=False):
122 """Private helper for overriding ``os_primary`` Tempest credentials.
123
Felipe Monteiro07a1c172017-12-10 04:26:08 +0000124 :param test_obj: instance of :py:class:`tempest.test.BaseTestCase`
Felipe Monteiro10e82fd2017-11-21 01:47:20 +0000125 :param toggle_rbac_role: Boolean value that controls the role that
126 overrides default role of ``os_primary`` credentials.
127 * If True: role is set to ``[patrole] rbac_test_role``
128 * If False: role is set to ``[identity] admin_role``
129 """
Felipe Monteiro10e82fd2017-11-21 01:47:20 +0000130 LOG.debug('Overriding role to: %s.', toggle_rbac_role)
Mykola Yakovlieve0f35502018-09-26 18:26:57 -0500131 roles_already_present = False
Felipe Monteiro2693bf72017-08-12 22:56:47 +0100132
DavidPurcell029d8c32017-01-06 15:27:41 -0500133 try:
Mykola Yakovlieve0f35502018-09-26 18:26:57 -0500134 if not all([self.admin_role_id, self.rbac_role_ids]):
Felipe Monteiro2693bf72017-08-12 22:56:47 +0100135 self._get_roles_by_name()
DavidPurcell029d8c32017-01-06 15:27:41 -0500136
Mykola Yakovlieve0f35502018-09-26 18:26:57 -0500137 target_roles = (self.rbac_role_ids
138 if toggle_rbac_role else [self.admin_role_id])
139 roles_already_present = self._list_and_clear_user_roles_on_project(
140 target_roles)
Felipe Monteiro2693bf72017-08-12 22:56:47 +0100141
Felipe Monteiro10e82fd2017-11-21 01:47:20 +0000142 # Do not override roles if `target_role` already exists.
Mykola Yakovlieve0f35502018-09-26 18:26:57 -0500143 if not roles_already_present:
144 self._create_user_role_on_project(target_roles)
DavidPurcell029d8c32017-01-06 15:27:41 -0500145 except Exception as exp:
Felipe Monteiro2693bf72017-08-12 22:56:47 +0100146 with excutils.save_and_reraise_exception():
147 LOG.exception(exp)
Felipe Monteiro34a138c2017-03-02 17:01:37 -0500148 finally:
Mykola Yakovliev1d829782018-08-03 14:37:37 -0500149 auth_providers = test_obj.get_auth_providers()
150 for provider in auth_providers:
151 provider.clear_auth()
Felipe Monteiro7be94e82017-07-26 02:17:08 +0100152 # Fernet tokens are not subsecond aware so sleep to ensure we are
153 # passing the second boundary before attempting to authenticate.
Felipe Monteiro2693bf72017-08-12 22:56:47 +0100154 # Only sleep if a token revocation occurred as a result of role
Felipe Monteiro10e82fd2017-11-21 01:47:20 +0000155 # overriding. This will optimize test runtime in the case where
Mykola Yakovlieve0f35502018-09-26 18:26:57 -0500156 # ``[identity] admin_role`` == ``[patrole] rbac_test_roles``.
157 if not roles_already_present:
Rick Bartra89f498f2017-03-20 15:54:45 -0400158 time.sleep(1)
Mykola Yakovliev1d829782018-08-03 14:37:37 -0500159
160 for provider in auth_providers:
161 provider.set_auth()
Felipe Monteiro34a138c2017-03-02 17:01:37 -0500162
Felipe Monteiro2693bf72017-08-12 22:56:47 +0100163 def _get_roles_by_name(self):
Felipe Monteiro2fc29292018-06-15 18:26:27 -0400164 available_roles = self.admin_roles_client.list_roles()['roles']
165 role_map = {r['name']: r['id'] for r in available_roles}
166 LOG.debug('Available roles: %s', list(role_map.keys()))
Felipe Monteirofa01d5f2017-04-01 06:18:25 +0100167
Mykola Yakovlieve0f35502018-09-26 18:26:57 -0500168 rbac_role_ids = []
169 roles = CONF.patrole.rbac_test_roles
170 # TODO(vegasq) drop once CONF.patrole.rbac_test_role is removed
171 if CONF.patrole.rbac_test_role:
172 if not roles:
173 roles.append(CONF.patrole.rbac_test_role)
Felipe Monteiro2693bf72017-08-12 22:56:47 +0100174
Mykola Yakovlieve0f35502018-09-26 18:26:57 -0500175 for role_name in roles:
176 rbac_role_ids.append(role_map.get(role_name))
177
178 admin_role_id = role_map.get(CONF.identity.admin_role)
179
180 if not all([admin_role_id, all(rbac_role_ids)]):
Felipe Monteiro2fc29292018-06-15 18:26:27 -0400181 missing_roles = []
Mykola Yakovlieve0f35502018-09-26 18:26:57 -0500182 msg = ("Could not find `[patrole] rbac_test_roles` or "
Felipe Monteiro2fc29292018-06-15 18:26:27 -0400183 "`[identity] admin_role`, both of which are required for "
184 "RBAC testing.")
185 if not admin_role_id:
186 missing_roles.append(CONF.identity.admin_role)
Mykola Yakovlieve0f35502018-09-26 18:26:57 -0500187 if not all(rbac_role_ids):
188 missing_roles += [role_name for role_name in roles
189 if not role_map.get(role_name)]
190
Felipe Monteiro2fc29292018-06-15 18:26:27 -0400191 msg += " Following roles were not found: %s." % (
192 ", ".join(missing_roles))
193 msg += " Available roles: %s." % ", ".join(list(role_map.keys()))
Felipe Monteiro2693bf72017-08-12 22:56:47 +0100194 raise rbac_exceptions.RbacResourceSetupFailed(msg)
195
196 self.admin_role_id = admin_role_id
Mykola Yakovlieve0f35502018-09-26 18:26:57 -0500197 self.rbac_role_ids = rbac_role_ids
Felipe Monteiro2693bf72017-08-12 22:56:47 +0100198
Mykola Yakovlieve0f35502018-09-26 18:26:57 -0500199 def _create_user_role_on_project(self, role_ids):
200 for role_id in role_ids:
201 self.admin_roles_client.create_user_role_on_project(
202 self.project_id, self.user_id, role_id)
Felipe Monteirofa01d5f2017-04-01 06:18:25 +0100203
Mykola Yakovlieve0f35502018-09-26 18:26:57 -0500204 def _list_and_clear_user_roles_on_project(self, role_ids):
Felipe Monteiroe8d93e02017-07-19 20:52:20 +0100205 roles = self.admin_roles_client.list_user_roles_on_project(
Felipe Monteirofa01d5f2017-04-01 06:18:25 +0100206 self.project_id, self.user_id)['roles']
Mykola Yakovlieve0f35502018-09-26 18:26:57 -0500207 all_role_ids = [role['id'] for role in roles]
Felipe Monteiro2693bf72017-08-12 22:56:47 +0100208
Mykola Yakovlieve0f35502018-09-26 18:26:57 -0500209 # NOTE(felipemonteiro): We do not use ``role_id in all_role_ids`` here
210 # to avoid over-permission errors: if the current list of roles on the
Felipe Monteiro2693bf72017-08-12 22:56:47 +0100211 # project includes "admin" and "Member", and we are switching to the
212 # "Member" role, then we must delete the "admin" role. Thus, we only
213 # return early if the user's roles on the project are an exact match.
Mykola Yakovlieve0f35502018-09-26 18:26:57 -0500214 if set(role_ids) == set(all_role_ids):
Felipe Monteirofa01d5f2017-04-01 06:18:25 +0100215 return True
Felipe Monteirob3b7bc82017-03-03 15:58:15 -0500216
217 for role in roles:
Felipe Monteiroe8d93e02017-07-19 20:52:20 +0100218 self.admin_roles_client.delete_role_from_user_on_project(
Felipe Monteirofa01d5f2017-04-01 06:18:25 +0100219 self.project_id, self.user_id, role['id'])
220
221 return False
222
Felipe Monteiro17e9b492017-05-27 05:45:20 +0100223
Felipe Monteiro07a1c172017-12-10 04:26:08 +0000224class RbacUtilsMixin(object):
225 """Mixin class to be used alongside an instance of
226 :py:class:`tempest.test.BaseTestCase`.
227
228 Should be used to perform Patrole class setup for a base RBAC class. Child
229 classes should not use this mixin.
230
231 Example::
232
233 class BaseRbacTest(rbac_utils.RbacUtilsMixin, base.BaseV2ComputeTest):
234
235 @classmethod
236 def skip_checks(cls):
237 super(BaseRbacTest, cls).skip_checks()
238 cls.skip_rbac_checks()
239
240 @classmethod
241 def setup_clients(cls):
242 super(BaseRbacTest, cls).setup_clients()
243 cls.setup_rbac_utils()
244 """
245
Mykola Yakovliev11376ab2018-08-06 15:34:22 -0500246 # Shows if override_role was called.
247 __override_role_called = False
248 # Shows if exception raised during override_role.
249 __override_role_caught_exc = False
250
Felipe Monteiro07a1c172017-12-10 04:26:08 +0000251 @classmethod
Mykola Yakovliev1d829782018-08-03 14:37:37 -0500252 def get_auth_providers(cls):
253 """Returns list of auth_providers used within test.
254
255 Tests may redefine this method to include their own or third party
256 client auth_providers.
257 """
258 return [cls.os_primary.auth_provider]
259
260 @classmethod
Felipe Monteiro07a1c172017-12-10 04:26:08 +0000261 def setup_rbac_utils(cls):
262 cls.rbac_utils = RbacUtils(cls)
263
Mykola Yakovliev11376ab2018-08-06 15:34:22 -0500264 def _set_override_role_called(self):
265 """Helper for tracking whether ``override_role`` was called."""
266 self.__override_role_called = True
267
268 def _set_override_role_caught_exc(self):
269 """Helper for tracking whether exception was thrown inside
270 ``override_role``.
271 """
272 self.__override_role_caught_exc = True
273
274 def _validate_override_role_called(self):
275 """Idempotently validate that ``override_role`` is called and reset
276 its value to False for sequential tests.
277 """
278 was_called = self.__override_role_called
279 self.__override_role_called = False
280 return was_called
281
282 def _validate_override_role_caught_exc(self):
283 """Idempotently validate that exception was caught inside
284 ``override_role``, so that, by process of elimination, it can be
285 determined whether one was thrown outside (which is invalid).
286 """
287 caught_exception = self.__override_role_caught_exc
288 self.__override_role_caught_exc = False
289 return caught_exception
290
Felipe Monteiro07a1c172017-12-10 04:26:08 +0000291
Felipe Monteiro8a043fb2017-08-06 06:29:05 +0100292def is_admin():
293 """Verifies whether the current test role equals the admin role.
294
Mykola Yakovlieve0f35502018-09-26 18:26:57 -0500295 :returns: True if ``rbac_test_roles`` contain the admin role.
Felipe Monteiro8a043fb2017-08-06 06:29:05 +0100296 """
Mykola Yakovlieve0f35502018-09-26 18:26:57 -0500297 roles = CONF.patrole.rbac_test_roles
298 # TODO(vegasq) drop once CONF.patrole.rbac_test_role is removed
299 if CONF.patrole.rbac_test_role:
300 roles.append(CONF.patrole.rbac_test_role)
301 roles = list(set(roles))
302
Felipe Monteiro2fc29292018-06-15 18:26:27 -0400303 # TODO(felipemonteiro): Make this more robust via a context is admin
304 # lookup.
Mykola Yakovlieve0f35502018-09-26 18:26:57 -0500305 return CONF.identity.admin_role in roles