blob: 9fa37407c9ef8a44fe262b39fc1b9c7eef8ee789 [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
Rick Bartraed950052017-06-29 17:20:33 -040016import abc
17import six
Felipe Monteirofa01d5f2017-04-01 06:18:25 +010018import sys
DavidPurcell029d8c32017-01-06 15:27:41 -050019import time
Felipe Monteiro34a138c2017-03-02 17:01:37 -050020
Rajiv Kumar645dfc92017-01-19 13:48:27 +053021from oslo_log import log as logging
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 Monteiroe8d93e02017-07-19 20:52:20 +010035 """Utility class responsible for switching os_primary role.
36
37 This class is responsible for overriding the value of the primary Tempest
38 credential's role (i.e. "os_primary" role). By doing so, it is possible to
39 seamlessly swap between admin credentials, needed for setup and clean up,
40 and primary credentials, needed to perform the API call which does
41 policy enforcement. The primary credentials always cycle between roles
Felipe Monteirof6eb8622017-08-06 06:08:02 +010042 defined by ``CONF.identity.admin_role`` and `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 Monteirob35de582017-05-05 00:16:53 +010059 self.switch_role(test_obj, toggle_rbac_role=False)
60
Felipe Monteiro75f23632017-04-07 15:56:26 +010061 # References the last value of `toggle_rbac_role` that was passed to
Felipe Monteirofa01d5f2017-04-01 06:18:25 +010062 # `switch_role`. Used for ensuring that `switch_role` is correctly used
63 # in a test file, so that false positives are prevented. The key used
Felipe Monteiro521e5c12017-04-05 22:59:57 +010064 # to index into the dictionary is the module path plus class name, which is
65 # unique.
Felipe Monteirofa01d5f2017-04-01 06:18:25 +010066 switch_role_history = {}
67 admin_role_id = None
68 rbac_role_id = None
DavidPurcell029d8c32017-01-06 15:27:41 -050069
Felipe Monteiro75f23632017-04-07 15:56:26 +010070 def switch_role(self, test_obj, toggle_rbac_role=False):
Felipe Monteiro7be94e82017-07-26 02:17:08 +010071 """Switch the role used by `os_primary` Tempest credentials.
72
73 Switch the role used by `os_primary` credentials to:
74 * admin if `toggle_rbac_role` is False
Felipe Monteirof6eb8622017-08-06 06:08:02 +010075 * `CONF.patrole.rbac_test_role` if `toggle_rbac_role` is True
Felipe Monteiro7be94e82017-07-26 02:17:08 +010076
Felipe Monteirof6eb8622017-08-06 06:08:02 +010077 :param test_obj: test object of type tempest.lib.base.BaseTestCase
78 :param toggle_rbac_role: role to switch `os_primary` Tempest creds to
Felipe Monteiro7be94e82017-07-26 02:17:08 +010079 """
Felipe Monteiroe8d93e02017-07-19 20:52:20 +010080 self.user_id = test_obj.os_primary.credentials.user_id
81 self.project_id = test_obj.os_primary.credentials.tenant_id
82 self.token = test_obj.os_primary.auth_provider.get_token()
DavidPurcell029d8c32017-01-06 15:27:41 -050083
Felipe Monteiroe8d93e02017-07-19 20:52:20 +010084 LOG.debug('Switching role to: %s.', toggle_rbac_role)
Felipe Monteiro2693bf72017-08-12 22:56:47 +010085 role_already_present = False
86
DavidPurcell029d8c32017-01-06 15:27:41 -050087 try:
Felipe Monteiro2693bf72017-08-12 22:56:47 +010088 if not all([self.admin_role_id, self.rbac_role_id]):
89 self._get_roles_by_name()
DavidPurcell029d8c32017-01-06 15:27:41 -050090
Felipe Monteirob35de582017-05-05 00:16:53 +010091 self._validate_switch_role(test_obj, toggle_rbac_role)
DavidPurcell029d8c32017-01-06 15:27:41 -050092
Felipe Monteiro2693bf72017-08-12 22:56:47 +010093 target_role = (
94 self.rbac_role_id if toggle_rbac_role else self.admin_role_id)
95 role_already_present = self._list_and_clear_user_roles_on_project(
96 target_role)
97
98 # Do not switch roles if `target_role` already exists.
99 if not role_already_present:
100 self._create_user_role_on_project(target_role)
DavidPurcell029d8c32017-01-06 15:27:41 -0500101 except Exception as exp:
Felipe Monteiro2693bf72017-08-12 22:56:47 +0100102 with excutils.save_and_reraise_exception():
103 LOG.exception(exp)
Felipe Monteiro34a138c2017-03-02 17:01:37 -0500104 finally:
Felipe Monteiroe8d93e02017-07-19 20:52:20 +0100105 test_obj.os_primary.auth_provider.clear_auth()
Felipe Monteiro7be94e82017-07-26 02:17:08 +0100106 # Fernet tokens are not subsecond aware so sleep to ensure we are
107 # passing the second boundary before attempting to authenticate.
Felipe Monteiro2693bf72017-08-12 22:56:47 +0100108 # Only sleep if a token revocation occurred as a result of role
109 # switching. This will optimize test runtime in the case where
110 # ``[identity] admin_role`` == ``[rbac] rbac_test_role``.
111 if not role_already_present:
Rick Bartra89f498f2017-03-20 15:54:45 -0400112 time.sleep(1)
Felipe Monteiroe8d93e02017-07-19 20:52:20 +0100113 test_obj.os_primary.auth_provider.set_auth()
Felipe Monteiro34a138c2017-03-02 17:01:37 -0500114
Felipe Monteiro2693bf72017-08-12 22:56:47 +0100115 def _get_roles_by_name(self):
116 available_roles = self.admin_roles_client.list_roles()
117 admin_role_id = rbac_role_id = None
Felipe Monteirofa01d5f2017-04-01 06:18:25 +0100118
Felipe Monteiro2693bf72017-08-12 22:56:47 +0100119 for role in available_roles['roles']:
Felipe Monteirof6eb8622017-08-06 06:08:02 +0100120 if role['name'] == CONF.patrole.rbac_test_role:
Felipe Monteiro2693bf72017-08-12 22:56:47 +0100121 rbac_role_id = role['id']
122 if role['name'] == CONF.identity.admin_role:
123 admin_role_id = role['id']
124
125 if not all([admin_role_id, rbac_role_id]):
Felipe Monteirof6eb8622017-08-06 06:08:02 +0100126 msg = ("Roles defined by `[patrole] rbac_test_role` and "
127 "`[identity] admin_role` must be defined in the system.")
Felipe Monteiro2693bf72017-08-12 22:56:47 +0100128 raise rbac_exceptions.RbacResourceSetupFailed(msg)
129
130 self.admin_role_id = admin_role_id
131 self.rbac_role_id = rbac_role_id
132
133 def _create_user_role_on_project(self, role_id):
Felipe Monteiroe8d93e02017-07-19 20:52:20 +0100134 self.admin_roles_client.create_user_role_on_project(
Felipe Monteirofa01d5f2017-04-01 06:18:25 +0100135 self.project_id, self.user_id, role_id)
136
Felipe Monteiro2693bf72017-08-12 22:56:47 +0100137 def _list_and_clear_user_roles_on_project(self, role_id):
Felipe Monteiroe8d93e02017-07-19 20:52:20 +0100138 roles = self.admin_roles_client.list_user_roles_on_project(
Felipe Monteirofa01d5f2017-04-01 06:18:25 +0100139 self.project_id, self.user_id)['roles']
Felipe Monteirofa01d5f2017-04-01 06:18:25 +0100140 role_ids = [role['id'] for role in roles]
Felipe Monteiro2693bf72017-08-12 22:56:47 +0100141
142 # NOTE(felipemonteiro): We do not use ``role_id in role_ids`` here to
143 # avoid over-permission errors: if the current list of roles on the
144 # project includes "admin" and "Member", and we are switching to the
145 # "Member" role, then we must delete the "admin" role. Thus, we only
146 # return early if the user's roles on the project are an exact match.
147 if [role_id] == role_ids:
Felipe Monteirofa01d5f2017-04-01 06:18:25 +0100148 return True
Felipe Monteirob3b7bc82017-03-03 15:58:15 -0500149
150 for role in roles:
Felipe Monteiroe8d93e02017-07-19 20:52:20 +0100151 self.admin_roles_client.delete_role_from_user_on_project(
Felipe Monteirofa01d5f2017-04-01 06:18:25 +0100152 self.project_id, self.user_id, role['id'])
153
154 return False
155
Felipe Monteiro75f23632017-04-07 15:56:26 +0100156 def _validate_switch_role(self, test_obj, toggle_rbac_role):
Felipe Monteiro2693bf72017-08-12 22:56:47 +0100157 """Validates that the test role passed to `switch_role` is legal.
Felipe Monteirofa01d5f2017-04-01 06:18:25 +0100158
159 Throws an error for the following improper usages of `switch_role`:
160 * `switch_role` is not called with a boolean value
Felipe Monteiro2693bf72017-08-12 22:56:47 +0100161 * `switch_role` is never called inside a test, except in tearDown
Felipe Monteirofa01d5f2017-04-01 06:18:25 +0100162 * `switch_role` is called with the same boolean value twice
Felipe Monteiro7be94e82017-07-26 02:17:08 +0100163
164 If a `skipException` is thrown then this is a legitimate reason why
165 `switch_role` is not called.
Felipe Monteirofa01d5f2017-04-01 06:18:25 +0100166 """
Felipe Monteiro75f23632017-04-07 15:56:26 +0100167 if not isinstance(toggle_rbac_role, bool):
Felipe Monteirofa01d5f2017-04-01 06:18:25 +0100168 raise rbac_exceptions.RbacResourceSetupFailed(
Felipe Monteiro2693bf72017-08-12 22:56:47 +0100169 '`toggle_rbac_role` must be a boolean value.')
Felipe Monteirofa01d5f2017-04-01 06:18:25 +0100170
Felipe Monteiro521e5c12017-04-05 22:59:57 +0100171 # The unique key is the combination of module path plus class name.
172 class_name = test_obj.__name__ if isinstance(test_obj, type) else \
Felipe Monteirofa01d5f2017-04-01 06:18:25 +0100173 test_obj.__class__.__name__
Felipe Monteiro521e5c12017-04-05 22:59:57 +0100174 module_name = test_obj.__module__
175 key = '%s.%s' % (module_name, class_name)
176
Felipe Monteirofa01d5f2017-04-01 06:18:25 +0100177 self.switch_role_history.setdefault(key, None)
178
Felipe Monteiro75f23632017-04-07 15:56:26 +0100179 if self.switch_role_history[key] == toggle_rbac_role:
Felipe Monteiroba4881b2017-04-09 02:11:25 +0100180 # If an exception was thrown, like a skipException or otherwise,
181 # then this is a legitimate reason why `switch_role` was not
182 # called, so only raise an exception if no current exception is
183 # being handled.
184 if sys.exc_info()[0] is None:
Felipe Monteirofa01d5f2017-04-01 06:18:25 +0100185 self.switch_role_history[key] = False
Felipe Monteiro75f23632017-04-07 15:56:26 +0100186 error_message = '`toggle_rbac_role` must not be called with '\
Felipe Monteirofa01d5f2017-04-01 06:18:25 +0100187 'the same bool value twice. Make sure that you included '\
188 'a rbac_utils.switch_role method call inside the test.'
189 LOG.error(error_message)
190 raise rbac_exceptions.RbacResourceSetupFailed(error_message)
191 else:
Felipe Monteiro75f23632017-04-07 15:56:26 +0100192 self.switch_role_history[key] = toggle_rbac_role
Felipe Monteirofa01d5f2017-04-01 06:18:25 +0100193
Felipe Monteiro17e9b492017-05-27 05:45:20 +0100194
Felipe Monteiro8a043fb2017-08-06 06:29:05 +0100195def is_admin():
196 """Verifies whether the current test role equals the admin role.
197
198 :returns: True if ``rbac_test_role`` is the admin role.
199 """
Felipe Monteirof6eb8622017-08-06 06:08:02 +0100200 return CONF.patrole.rbac_test_role == CONF.identity.admin_role
Rick Bartraed950052017-06-29 17:20:33 -0400201
202
203@six.add_metaclass(abc.ABCMeta)
204class RbacAuthority(object):
Felipe Monteiro2a3b5132017-08-11 01:19:06 +0100205 """Class for validating whether a given role can perform a policy action.
206
207 Any class that extends ``RbacAuthority`` provides the logic for determining
208 whether a role has permissions to execute a policy action.
209 """
Rick Bartraed950052017-06-29 17:20:33 -0400210
211 @abc.abstractmethod
Felipe Monteiro2a3b5132017-08-11 01:19:06 +0100212 def allowed(self, rule, role):
213 """Determine whether the role should be able to perform the API.
214
215 :param rule: The name of the policy enforced by the API.
216 :param role: The role used to determine whether ``rule`` can be
217 executed.
218 :returns: True if the ``role`` has permissions to execute
219 ``rule``, else False.
220 """