blob: c928f40fce4e88bdc933f58b1745387295a91771 [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 Monteiro83903412018-07-09 16:33:55 +010021from oslo_log import versionutils
Felipe Monteiro2693bf72017-08-12 22:56:47 +010022from oslo_utils import excutils
Felipe Monteirofa01d5f2017-04-01 06:18:25 +010023
Felipe Monteiro3e14f472017-08-17 23:02:11 +010024from tempest import clients
Felipe Monteiroe7e552e2017-05-02 17:04:12 +010025from tempest.common import credentials_factory as credentials
Felipe Monteirofa01d5f2017-04-01 06:18:25 +010026from tempest import config
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
43 ``CONF.patrole.rbac_test_role``.
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 Monteiroe8d93e02017-07-19 20:52:20 +010054 if test_obj.get_identity_version() == '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 Monteiro3e14f472017-08-17 23:02:11 +010057 admin_roles_client = admin_mgr.roles_client
Felipe Monteiroe8d93e02017-07-19 20:52:20 +010058
59 self.admin_roles_client = admin_roles_client
Felipe Monteiro10e82fd2017-11-21 01:47:20 +000060 self._override_role(test_obj, False)
Felipe Monteirob35de582017-05-05 00:16:53 +010061
Felipe Monteirofa01d5f2017-04-01 06:18:25 +010062 admin_role_id = None
63 rbac_role_id = None
DavidPurcell029d8c32017-01-06 15:27:41 -050064
Felipe Monteiro10e82fd2017-11-21 01:47:20 +000065 @contextmanager
66 def override_role(self, test_obj):
67 """Override the role used by ``os_primary`` Tempest credentials.
68
69 Temporarily change the role used by ``os_primary`` credentials to:
Masayuki Igawa80b9aab2018-01-09 17:00:45 +090070
71 * ``[patrole] rbac_test_role`` before test execution
72 * ``[identity] admin_role`` after test execution
Felipe Monteiro10e82fd2017-11-21 01:47:20 +000073
74 Automatically switches to admin role after test execution.
75
76 :param test_obj: Instance of ``tempest.test.BaseTestCase``.
77 :returns: None
78
79 .. warning::
80
81 This function can alter user roles for pre-provisioned credentials.
82 Work is underway to safely clean up after this function.
83
84 Example::
85
86 @rbac_rule_validation.action(service='test',
87 rule='a:test:rule')
88 def test_foo(self):
89 # Allocate test-level resources here.
90 with self.rbac_utils.override_role(self):
melissaml7cd21612018-05-23 21:00:50 +080091 # The role for `os_primary` has now been overridden. Within
Felipe Monteiro10e82fd2017-11-21 01:47:20 +000092 # this block, call the API endpoint that enforces the
93 # expected policy specified by "rule" in the decorator.
94 self.foo_service.bar_api_call()
95 # The role is switched back to admin automatically. Note that
96 # if the API call above threw an exception, any code below this
97 # point in the test is not executed.
98 """
Mykola Yakovliev11376ab2018-08-06 15:34:22 -050099 test_obj._set_override_role_called()
Felipe Monteiro10e82fd2017-11-21 01:47:20 +0000100 self._override_role(test_obj, True)
101 try:
102 # Execute the test.
103 yield
104 finally:
Mykola Yakovliev11376ab2018-08-06 15:34:22 -0500105 # Check whether an exception was raised. If so, remember that
106 # for future validation.
107 exc = sys.exc_info()[0]
108 if exc is not None:
109 test_obj._set_override_role_caught_exc()
Felipe Monteiro10e82fd2017-11-21 01:47:20 +0000110 # This code block is always executed, no matter the result of the
111 # test. Automatically switch back to the admin role for test clean
112 # up.
113 self._override_role(test_obj, False)
114
Felipe Monteiro10e82fd2017-11-21 01:47:20 +0000115 def _override_role(self, test_obj, toggle_rbac_role=False):
116 """Private helper for overriding ``os_primary`` Tempest credentials.
117
Felipe Monteiro07a1c172017-12-10 04:26:08 +0000118 :param test_obj: instance of :py:class:`tempest.test.BaseTestCase`
Felipe Monteiro10e82fd2017-11-21 01:47:20 +0000119 :param toggle_rbac_role: Boolean value that controls the role that
120 overrides default role of ``os_primary`` credentials.
121 * If True: role is set to ``[patrole] rbac_test_role``
122 * If False: role is set to ``[identity] admin_role``
123 """
Felipe Monteiroe8d93e02017-07-19 20:52:20 +0100124 self.user_id = test_obj.os_primary.credentials.user_id
125 self.project_id = test_obj.os_primary.credentials.tenant_id
126 self.token = test_obj.os_primary.auth_provider.get_token()
DavidPurcell029d8c32017-01-06 15:27:41 -0500127
Felipe Monteiro10e82fd2017-11-21 01:47:20 +0000128 LOG.debug('Overriding role to: %s.', toggle_rbac_role)
Felipe Monteiro2693bf72017-08-12 22:56:47 +0100129 role_already_present = False
130
DavidPurcell029d8c32017-01-06 15:27:41 -0500131 try:
Felipe Monteiro2693bf72017-08-12 22:56:47 +0100132 if not all([self.admin_role_id, self.rbac_role_id]):
133 self._get_roles_by_name()
DavidPurcell029d8c32017-01-06 15:27:41 -0500134
Felipe Monteiro2693bf72017-08-12 22:56:47 +0100135 target_role = (
136 self.rbac_role_id if toggle_rbac_role else self.admin_role_id)
137 role_already_present = self._list_and_clear_user_roles_on_project(
138 target_role)
139
Felipe Monteiro10e82fd2017-11-21 01:47:20 +0000140 # Do not override roles if `target_role` already exists.
Felipe Monteiro2693bf72017-08-12 22:56:47 +0100141 if not role_already_present:
142 self._create_user_role_on_project(target_role)
DavidPurcell029d8c32017-01-06 15:27:41 -0500143 except Exception as exp:
Felipe Monteiro2693bf72017-08-12 22:56:47 +0100144 with excutils.save_and_reraise_exception():
145 LOG.exception(exp)
Felipe Monteiro34a138c2017-03-02 17:01:37 -0500146 finally:
Mykola Yakovliev1d829782018-08-03 14:37:37 -0500147 auth_providers = test_obj.get_auth_providers()
148 for provider in auth_providers:
149 provider.clear_auth()
Felipe Monteiro7be94e82017-07-26 02:17:08 +0100150 # Fernet tokens are not subsecond aware so sleep to ensure we are
151 # passing the second boundary before attempting to authenticate.
Felipe Monteiro2693bf72017-08-12 22:56:47 +0100152 # Only sleep if a token revocation occurred as a result of role
Felipe Monteiro10e82fd2017-11-21 01:47:20 +0000153 # overriding. This will optimize test runtime in the case where
Felipe Monteirob58c1192017-11-20 01:50:24 +0000154 # ``[identity] admin_role`` == ``[patrole] rbac_test_role``.
Felipe Monteiro2693bf72017-08-12 22:56:47 +0100155 if not role_already_present:
Rick Bartra89f498f2017-03-20 15:54:45 -0400156 time.sleep(1)
Mykola Yakovliev1d829782018-08-03 14:37:37 -0500157
158 for provider in auth_providers:
159 provider.set_auth()
Felipe Monteiro34a138c2017-03-02 17:01:37 -0500160
Felipe Monteiro2693bf72017-08-12 22:56:47 +0100161 def _get_roles_by_name(self):
Felipe Monteiro2fc29292018-06-15 18:26:27 -0400162 available_roles = self.admin_roles_client.list_roles()['roles']
163 role_map = {r['name']: r['id'] for r in available_roles}
164 LOG.debug('Available roles: %s', list(role_map.keys()))
Felipe Monteirofa01d5f2017-04-01 06:18:25 +0100165
Felipe Monteiro2fc29292018-06-15 18:26:27 -0400166 admin_role_id = role_map.get(CONF.identity.admin_role)
167 rbac_role_id = role_map.get(CONF.patrole.rbac_test_role)
Felipe Monteiro2693bf72017-08-12 22:56:47 +0100168
169 if not all([admin_role_id, rbac_role_id]):
Felipe Monteiro2fc29292018-06-15 18:26:27 -0400170 missing_roles = []
171 msg = ("Could not find `[patrole] rbac_test_role` or "
172 "`[identity] admin_role`, both of which are required for "
173 "RBAC testing.")
174 if not admin_role_id:
175 missing_roles.append(CONF.identity.admin_role)
176 if not rbac_role_id:
177 missing_roles.append(CONF.patrole.rbac_test_role)
178 msg += " Following roles were not found: %s." % (
179 ", ".join(missing_roles))
180 msg += " Available roles: %s." % ", ".join(list(role_map.keys()))
Felipe Monteiro2693bf72017-08-12 22:56:47 +0100181 raise rbac_exceptions.RbacResourceSetupFailed(msg)
182
183 self.admin_role_id = admin_role_id
184 self.rbac_role_id = rbac_role_id
185
186 def _create_user_role_on_project(self, role_id):
Felipe Monteiroe8d93e02017-07-19 20:52:20 +0100187 self.admin_roles_client.create_user_role_on_project(
Felipe Monteirofa01d5f2017-04-01 06:18:25 +0100188 self.project_id, self.user_id, role_id)
189
Felipe Monteiro2693bf72017-08-12 22:56:47 +0100190 def _list_and_clear_user_roles_on_project(self, role_id):
Felipe Monteiroe8d93e02017-07-19 20:52:20 +0100191 roles = self.admin_roles_client.list_user_roles_on_project(
Felipe Monteirofa01d5f2017-04-01 06:18:25 +0100192 self.project_id, self.user_id)['roles']
Felipe Monteirofa01d5f2017-04-01 06:18:25 +0100193 role_ids = [role['id'] for role in roles]
Felipe Monteiro2693bf72017-08-12 22:56:47 +0100194
195 # NOTE(felipemonteiro): We do not use ``role_id in role_ids`` here to
196 # avoid over-permission errors: if the current list of roles on the
197 # project includes "admin" and "Member", and we are switching to the
198 # "Member" role, then we must delete the "admin" role. Thus, we only
199 # return early if the user's roles on the project are an exact match.
200 if [role_id] == role_ids:
Felipe Monteirofa01d5f2017-04-01 06:18:25 +0100201 return True
Felipe Monteirob3b7bc82017-03-03 15:58:15 -0500202
203 for role in roles:
Felipe Monteiroe8d93e02017-07-19 20:52:20 +0100204 self.admin_roles_client.delete_role_from_user_on_project(
Felipe Monteirofa01d5f2017-04-01 06:18:25 +0100205 self.project_id, self.user_id, role['id'])
206
207 return False
208
Felipe Monteiro17e9b492017-05-27 05:45:20 +0100209
Felipe Monteiro07a1c172017-12-10 04:26:08 +0000210class RbacUtilsMixin(object):
211 """Mixin class to be used alongside an instance of
212 :py:class:`tempest.test.BaseTestCase`.
213
214 Should be used to perform Patrole class setup for a base RBAC class. Child
215 classes should not use this mixin.
216
217 Example::
218
219 class BaseRbacTest(rbac_utils.RbacUtilsMixin, base.BaseV2ComputeTest):
220
221 @classmethod
222 def skip_checks(cls):
223 super(BaseRbacTest, cls).skip_checks()
224 cls.skip_rbac_checks()
225
226 @classmethod
227 def setup_clients(cls):
228 super(BaseRbacTest, cls).setup_clients()
229 cls.setup_rbac_utils()
230 """
231
Mykola Yakovliev11376ab2018-08-06 15:34:22 -0500232 # Shows if override_role was called.
233 __override_role_called = False
234 # Shows if exception raised during override_role.
235 __override_role_caught_exc = False
236
Felipe Monteiro07a1c172017-12-10 04:26:08 +0000237 @classmethod
Mykola Yakovliev1d829782018-08-03 14:37:37 -0500238 def get_auth_providers(cls):
239 """Returns list of auth_providers used within test.
240
241 Tests may redefine this method to include their own or third party
242 client auth_providers.
243 """
244 return [cls.os_primary.auth_provider]
245
246 @classmethod
Felipe Monteiro07a1c172017-12-10 04:26:08 +0000247 def skip_rbac_checks(cls):
248 if not CONF.patrole.enable_rbac:
Felipe Monteiro83903412018-07-09 16:33:55 +0100249 deprecation_msg = ("The `[patrole].enable_rbac` option is "
250 "deprecated and will be removed in the S "
251 "release. Patrole tests will always be enabled "
252 "following installation of the Patrole Tempest "
Mykola Yakovliev11376ab2018-08-06 15:34:22 -0500253 "plugin. Use a regex to skip tests")
Felipe Monteiro83903412018-07-09 16:33:55 +0100254 versionutils.report_deprecated_feature(LOG, deprecation_msg)
Felipe Monteiro07a1c172017-12-10 04:26:08 +0000255 raise cls.skipException(
Felipe Monteirod7371992018-04-25 16:57:09 +0100256 'Patrole testing not enabled so skipping %s.' % cls.__name__)
Felipe Monteiro07a1c172017-12-10 04:26:08 +0000257
258 @classmethod
259 def setup_rbac_utils(cls):
260 cls.rbac_utils = RbacUtils(cls)
261
Mykola Yakovliev11376ab2018-08-06 15:34:22 -0500262 def _set_override_role_called(self):
263 """Helper for tracking whether ``override_role`` was called."""
264 self.__override_role_called = True
265
266 def _set_override_role_caught_exc(self):
267 """Helper for tracking whether exception was thrown inside
268 ``override_role``.
269 """
270 self.__override_role_caught_exc = True
271
272 def _validate_override_role_called(self):
273 """Idempotently validate that ``override_role`` is called and reset
274 its value to False for sequential tests.
275 """
276 was_called = self.__override_role_called
277 self.__override_role_called = False
278 return was_called
279
280 def _validate_override_role_caught_exc(self):
281 """Idempotently validate that exception was caught inside
282 ``override_role``, so that, by process of elimination, it can be
283 determined whether one was thrown outside (which is invalid).
284 """
285 caught_exception = self.__override_role_caught_exc
286 self.__override_role_caught_exc = False
287 return caught_exception
288
Felipe Monteiro07a1c172017-12-10 04:26:08 +0000289
Felipe Monteiro8a043fb2017-08-06 06:29:05 +0100290def is_admin():
291 """Verifies whether the current test role equals the admin role.
292
293 :returns: True if ``rbac_test_role`` is the admin role.
294 """
Felipe Monteiro2fc29292018-06-15 18:26:27 -0400295 # TODO(felipemonteiro): Make this more robust via a context is admin
296 # lookup.
Felipe Monteirof6eb8622017-08-06 06:08:02 +0100297 return CONF.patrole.rbac_test_role == CONF.identity.admin_role