blob: 6db2199cf0e3dd94cf3922fd97ae07473071df7b [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
DavidPurcell029d8c32017-01-06 15:27:41 -050026
Felipe Monteiro34a138c2017-03-02 17:01:37 -050027from patrole_tempest_plugin import rbac_exceptions
DavidPurcell029d8c32017-01-06 15:27:41 -050028
DavidPurcell029d8c32017-01-06 15:27:41 -050029CONF = config.CONF
Felipe Monteiro34a138c2017-03-02 17:01:37 -050030LOG = logging.getLogger(__name__)
DavidPurcell029d8c32017-01-06 15:27:41 -050031
32
DavidPurcell029d8c32017-01-06 15:27:41 -050033class RbacUtils(object):
Felipe Monteiro10e82fd2017-11-21 01:47:20 +000034 """Utility class responsible for switching ``os_primary`` role.
Felipe Monteiroe8d93e02017-07-19 20:52:20 +010035
36 This class is responsible for overriding the value of the primary Tempest
Felipe Monteiro10e82fd2017-11-21 01:47:20 +000037 credential's role (i.e. ``os_primary`` role). By doing so, it is possible
38 to seamlessly swap between admin credentials, needed for setup and clean
39 up, and primary credentials, needed to perform the API call which does
Felipe Monteiroe8d93e02017-07-19 20:52:20 +010040 policy enforcement. The primary credentials always cycle between roles
Felipe Monteiro10e82fd2017-11-21 01:47:20 +000041 defined by ``CONF.identity.admin_role`` and
42 ``CONF.patrole.rbac_test_role``.
Felipe Monteiroe8d93e02017-07-19 20:52:20 +010043 """
DavidPurcell029d8c32017-01-06 15:27:41 -050044
Felipe Monteirob35de582017-05-05 00:16:53 +010045 def __init__(self, test_obj):
Felipe Monteiroe8d93e02017-07-19 20:52:20 +010046 """Constructor for ``RbacUtils``.
47
Felipe Monteiro2693bf72017-08-12 22:56:47 +010048 :param test_obj: An instance of `tempest.test.BaseTestCase`.
Felipe Monteiroe8d93e02017-07-19 20:52:20 +010049 """
Felipe Monteiroe8d93e02017-07-19 20:52:20 +010050 # Intialize the admin roles_client to perform role switching.
Felipe Monteiro3e14f472017-08-17 23:02:11 +010051 admin_mgr = clients.Manager(
52 credentials.get_configured_admin_credentials())
Felipe Monteiroe8d93e02017-07-19 20:52:20 +010053 if test_obj.get_identity_version() == 'v3':
Felipe Monteiro3e14f472017-08-17 23:02:11 +010054 admin_roles_client = admin_mgr.roles_v3_client
Felipe Monteiroe8d93e02017-07-19 20:52:20 +010055 else:
Felipe Monteiro3e14f472017-08-17 23:02:11 +010056 admin_roles_client = admin_mgr.roles_client
Felipe Monteiroe8d93e02017-07-19 20:52:20 +010057
58 self.admin_roles_client = admin_roles_client
Felipe Monteiro10e82fd2017-11-21 01:47:20 +000059 self._override_role(test_obj, False)
Felipe Monteirob35de582017-05-05 00:16:53 +010060
Felipe Monteirofa01d5f2017-04-01 06:18:25 +010061 admin_role_id = None
62 rbac_role_id = None
DavidPurcell029d8c32017-01-06 15:27:41 -050063
Felipe Monteiro10e82fd2017-11-21 01:47:20 +000064 @contextmanager
65 def override_role(self, test_obj):
66 """Override the role used by ``os_primary`` Tempest credentials.
67
68 Temporarily change the role used by ``os_primary`` credentials to:
Masayuki Igawa80b9aab2018-01-09 17:00:45 +090069
70 * ``[patrole] rbac_test_role`` before test execution
71 * ``[identity] admin_role`` after test execution
Felipe Monteiro10e82fd2017-11-21 01:47:20 +000072
73 Automatically switches to admin role after test execution.
74
75 :param test_obj: Instance of ``tempest.test.BaseTestCase``.
76 :returns: None
77
78 .. warning::
79
80 This function can alter user roles for pre-provisioned credentials.
81 Work is underway to safely clean up after this function.
82
83 Example::
84
85 @rbac_rule_validation.action(service='test',
Felipe Monteiro59f538f2018-08-22 23:34:40 -040086 rules=['a:test:rule'])
Felipe Monteiro10e82fd2017-11-21 01:47:20 +000087 def test_foo(self):
88 # Allocate test-level resources here.
89 with self.rbac_utils.override_role(self):
melissaml7cd21612018-05-23 21:00:50 +080090 # The role for `os_primary` has now been overridden. Within
Felipe Monteiro10e82fd2017-11-21 01:47:20 +000091 # this block, call the API endpoint that enforces the
92 # expected policy specified by "rule" in the decorator.
93 self.foo_service.bar_api_call()
94 # The role is switched back to admin automatically. Note that
95 # if the API call above threw an exception, any code below this
96 # point in the test is not executed.
97 """
Mykola Yakovliev11376ab2018-08-06 15:34:22 -050098 test_obj._set_override_role_called()
Felipe Monteiro10e82fd2017-11-21 01:47:20 +000099 self._override_role(test_obj, True)
100 try:
101 # Execute the test.
102 yield
103 finally:
Mykola Yakovliev11376ab2018-08-06 15:34:22 -0500104 # Check whether an exception was raised. If so, remember that
105 # for future validation.
106 exc = sys.exc_info()[0]
107 if exc is not None:
108 test_obj._set_override_role_caught_exc()
Felipe Monteiro10e82fd2017-11-21 01:47:20 +0000109 # This code block is always executed, no matter the result of the
110 # test. Automatically switch back to the admin role for test clean
111 # up.
112 self._override_role(test_obj, False)
113
Felipe Monteiro10e82fd2017-11-21 01:47:20 +0000114 def _override_role(self, test_obj, toggle_rbac_role=False):
115 """Private helper for overriding ``os_primary`` Tempest credentials.
116
Felipe Monteiro07a1c172017-12-10 04:26:08 +0000117 :param test_obj: instance of :py:class:`tempest.test.BaseTestCase`
Felipe Monteiro10e82fd2017-11-21 01:47:20 +0000118 :param toggle_rbac_role: Boolean value that controls the role that
119 overrides default role of ``os_primary`` credentials.
120 * If True: role is set to ``[patrole] rbac_test_role``
121 * If False: role is set to ``[identity] admin_role``
122 """
Felipe Monteiroe8d93e02017-07-19 20:52:20 +0100123 self.user_id = test_obj.os_primary.credentials.user_id
124 self.project_id = test_obj.os_primary.credentials.tenant_id
125 self.token = test_obj.os_primary.auth_provider.get_token()
DavidPurcell029d8c32017-01-06 15:27:41 -0500126
Felipe Monteiro10e82fd2017-11-21 01:47:20 +0000127 LOG.debug('Overriding role to: %s.', toggle_rbac_role)
Felipe Monteiro2693bf72017-08-12 22:56:47 +0100128 role_already_present = False
129
DavidPurcell029d8c32017-01-06 15:27:41 -0500130 try:
Felipe Monteiro2693bf72017-08-12 22:56:47 +0100131 if not all([self.admin_role_id, self.rbac_role_id]):
132 self._get_roles_by_name()
DavidPurcell029d8c32017-01-06 15:27:41 -0500133
Felipe Monteiro2693bf72017-08-12 22:56:47 +0100134 target_role = (
135 self.rbac_role_id if toggle_rbac_role else self.admin_role_id)
136 role_already_present = self._list_and_clear_user_roles_on_project(
137 target_role)
138
Felipe Monteiro10e82fd2017-11-21 01:47:20 +0000139 # Do not override roles if `target_role` already exists.
Felipe Monteiro2693bf72017-08-12 22:56:47 +0100140 if not role_already_present:
141 self._create_user_role_on_project(target_role)
DavidPurcell029d8c32017-01-06 15:27:41 -0500142 except Exception as exp:
Felipe Monteiro2693bf72017-08-12 22:56:47 +0100143 with excutils.save_and_reraise_exception():
144 LOG.exception(exp)
Felipe Monteiro34a138c2017-03-02 17:01:37 -0500145 finally:
Mykola Yakovliev1d829782018-08-03 14:37:37 -0500146 auth_providers = test_obj.get_auth_providers()
147 for provider in auth_providers:
148 provider.clear_auth()
Felipe Monteiro7be94e82017-07-26 02:17:08 +0100149 # Fernet tokens are not subsecond aware so sleep to ensure we are
150 # passing the second boundary before attempting to authenticate.
Felipe Monteiro2693bf72017-08-12 22:56:47 +0100151 # Only sleep if a token revocation occurred as a result of role
Felipe Monteiro10e82fd2017-11-21 01:47:20 +0000152 # overriding. This will optimize test runtime in the case where
Felipe Monteirob58c1192017-11-20 01:50:24 +0000153 # ``[identity] admin_role`` == ``[patrole] rbac_test_role``.
Felipe Monteiro2693bf72017-08-12 22:56:47 +0100154 if not role_already_present:
Rick Bartra89f498f2017-03-20 15:54:45 -0400155 time.sleep(1)
Mykola Yakovliev1d829782018-08-03 14:37:37 -0500156
157 for provider in auth_providers:
158 provider.set_auth()
Felipe Monteiro34a138c2017-03-02 17:01:37 -0500159
Felipe Monteiro2693bf72017-08-12 22:56:47 +0100160 def _get_roles_by_name(self):
Felipe Monteiro2fc29292018-06-15 18:26:27 -0400161 available_roles = self.admin_roles_client.list_roles()['roles']
162 role_map = {r['name']: r['id'] for r in available_roles}
163 LOG.debug('Available roles: %s', list(role_map.keys()))
Felipe Monteirofa01d5f2017-04-01 06:18:25 +0100164
Felipe Monteiro2fc29292018-06-15 18:26:27 -0400165 admin_role_id = role_map.get(CONF.identity.admin_role)
166 rbac_role_id = role_map.get(CONF.patrole.rbac_test_role)
Felipe Monteiro2693bf72017-08-12 22:56:47 +0100167
168 if not all([admin_role_id, rbac_role_id]):
Felipe Monteiro2fc29292018-06-15 18:26:27 -0400169 missing_roles = []
170 msg = ("Could not find `[patrole] rbac_test_role` or "
171 "`[identity] admin_role`, both of which are required for "
172 "RBAC testing.")
173 if not admin_role_id:
174 missing_roles.append(CONF.identity.admin_role)
175 if not rbac_role_id:
176 missing_roles.append(CONF.patrole.rbac_test_role)
177 msg += " Following roles were not found: %s." % (
178 ", ".join(missing_roles))
179 msg += " Available roles: %s." % ", ".join(list(role_map.keys()))
Felipe Monteiro2693bf72017-08-12 22:56:47 +0100180 raise rbac_exceptions.RbacResourceSetupFailed(msg)
181
182 self.admin_role_id = admin_role_id
183 self.rbac_role_id = rbac_role_id
184
185 def _create_user_role_on_project(self, role_id):
Felipe Monteiroe8d93e02017-07-19 20:52:20 +0100186 self.admin_roles_client.create_user_role_on_project(
Felipe Monteirofa01d5f2017-04-01 06:18:25 +0100187 self.project_id, self.user_id, role_id)
188
Felipe Monteiro2693bf72017-08-12 22:56:47 +0100189 def _list_and_clear_user_roles_on_project(self, role_id):
Felipe Monteiroe8d93e02017-07-19 20:52:20 +0100190 roles = self.admin_roles_client.list_user_roles_on_project(
Felipe Monteirofa01d5f2017-04-01 06:18:25 +0100191 self.project_id, self.user_id)['roles']
Felipe Monteirofa01d5f2017-04-01 06:18:25 +0100192 role_ids = [role['id'] for role in roles]
Felipe Monteiro2693bf72017-08-12 22:56:47 +0100193
194 # NOTE(felipemonteiro): We do not use ``role_id in role_ids`` here to
195 # avoid over-permission errors: if the current list of roles on the
196 # project includes "admin" and "Member", and we are switching to the
197 # "Member" role, then we must delete the "admin" role. Thus, we only
198 # return early if the user's roles on the project are an exact match.
199 if [role_id] == role_ids:
Felipe Monteirofa01d5f2017-04-01 06:18:25 +0100200 return True
Felipe Monteirob3b7bc82017-03-03 15:58:15 -0500201
202 for role in roles:
Felipe Monteiroe8d93e02017-07-19 20:52:20 +0100203 self.admin_roles_client.delete_role_from_user_on_project(
Felipe Monteirofa01d5f2017-04-01 06:18:25 +0100204 self.project_id, self.user_id, role['id'])
205
206 return False
207
Felipe Monteiro17e9b492017-05-27 05:45:20 +0100208
Felipe Monteiro07a1c172017-12-10 04:26:08 +0000209class RbacUtilsMixin(object):
210 """Mixin class to be used alongside an instance of
211 :py:class:`tempest.test.BaseTestCase`.
212
213 Should be used to perform Patrole class setup for a base RBAC class. Child
214 classes should not use this mixin.
215
216 Example::
217
218 class BaseRbacTest(rbac_utils.RbacUtilsMixin, base.BaseV2ComputeTest):
219
220 @classmethod
221 def skip_checks(cls):
222 super(BaseRbacTest, cls).skip_checks()
223 cls.skip_rbac_checks()
224
225 @classmethod
226 def setup_clients(cls):
227 super(BaseRbacTest, cls).setup_clients()
228 cls.setup_rbac_utils()
229 """
230
Mykola Yakovliev11376ab2018-08-06 15:34:22 -0500231 # Shows if override_role was called.
232 __override_role_called = False
233 # Shows if exception raised during override_role.
234 __override_role_caught_exc = False
235
Felipe Monteiro07a1c172017-12-10 04:26:08 +0000236 @classmethod
Mykola Yakovliev1d829782018-08-03 14:37:37 -0500237 def get_auth_providers(cls):
238 """Returns list of auth_providers used within test.
239
240 Tests may redefine this method to include their own or third party
241 client auth_providers.
242 """
243 return [cls.os_primary.auth_provider]
244
245 @classmethod
Felipe Monteiro07a1c172017-12-10 04:26:08 +0000246 def setup_rbac_utils(cls):
247 cls.rbac_utils = RbacUtils(cls)
248
Mykola Yakovliev11376ab2018-08-06 15:34:22 -0500249 def _set_override_role_called(self):
250 """Helper for tracking whether ``override_role`` was called."""
251 self.__override_role_called = True
252
253 def _set_override_role_caught_exc(self):
254 """Helper for tracking whether exception was thrown inside
255 ``override_role``.
256 """
257 self.__override_role_caught_exc = True
258
259 def _validate_override_role_called(self):
260 """Idempotently validate that ``override_role`` is called and reset
261 its value to False for sequential tests.
262 """
263 was_called = self.__override_role_called
264 self.__override_role_called = False
265 return was_called
266
267 def _validate_override_role_caught_exc(self):
268 """Idempotently validate that exception was caught inside
269 ``override_role``, so that, by process of elimination, it can be
270 determined whether one was thrown outside (which is invalid).
271 """
272 caught_exception = self.__override_role_caught_exc
273 self.__override_role_caught_exc = False
274 return caught_exception
275
Felipe Monteiro07a1c172017-12-10 04:26:08 +0000276
Felipe Monteiro8a043fb2017-08-06 06:29:05 +0100277def is_admin():
278 """Verifies whether the current test role equals the admin role.
279
280 :returns: True if ``rbac_test_role`` is the admin role.
281 """
Felipe Monteiro2fc29292018-06-15 18:26:27 -0400282 # TODO(felipemonteiro): Make this more robust via a context is admin
283 # lookup.
Felipe Monteirof6eb8622017-08-06 06:08:02 +0100284 return CONF.patrole.rbac_test_role == CONF.identity.admin_role