blob: 5543cbb2ee3d5c297b367275771c21a4d3706dba [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 Monteiroe8d93e02017-07-19 20:52:20 +010023import testtools
Felipe Monteirofa01d5f2017-04-01 06:18:25 +010024
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 Monteiro2693bf72017-08-12 22:56:47 +010042 defined by ``[identity] admin_role`` and ``[rbac] 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 """
50 # Since we are going to instantiate a client manager with
51 # admin credentials, first check if admin is available.
52 if not credentials.is_admin_available(
53 identity_version=test_obj.get_identity_version()):
54 msg = "Missing Identity Admin API credentials in configuration."
55 raise testtools.TestCase.skipException(msg)
56
57 # Intialize the admin roles_client to perform role switching.
58 admin_creds = test_obj.get_client_manager(credential_type='admin')
59 if test_obj.get_identity_version() == 'v3':
60 admin_roles_client = admin_creds.roles_v3_client
61 else:
62 admin_roles_client = admin_creds.roles_client
63
64 self.admin_roles_client = admin_roles_client
Felipe Monteirob35de582017-05-05 00:16:53 +010065 self.switch_role(test_obj, toggle_rbac_role=False)
66
Felipe Monteiro75f23632017-04-07 15:56:26 +010067 # References the last value of `toggle_rbac_role` that was passed to
Felipe Monteirofa01d5f2017-04-01 06:18:25 +010068 # `switch_role`. Used for ensuring that `switch_role` is correctly used
69 # in a test file, so that false positives are prevented. The key used
Felipe Monteiro521e5c12017-04-05 22:59:57 +010070 # to index into the dictionary is the module path plus class name, which is
71 # unique.
Felipe Monteirofa01d5f2017-04-01 06:18:25 +010072 switch_role_history = {}
73 admin_role_id = None
74 rbac_role_id = None
DavidPurcell029d8c32017-01-06 15:27:41 -050075
Felipe Monteiro75f23632017-04-07 15:56:26 +010076 def switch_role(self, test_obj, toggle_rbac_role=False):
Felipe Monteiro7be94e82017-07-26 02:17:08 +010077 """Switch the role used by `os_primary` Tempest credentials.
78
79 Switch the role used by `os_primary` credentials to:
80 * admin if `toggle_rbac_role` is False
Felipe Monteiro2693bf72017-08-12 22:56:47 +010081 * `[rbac] rbac_test_role` if `toggle_rbac_role` is True
Felipe Monteiro7be94e82017-07-26 02:17:08 +010082
Felipe Monteiro2693bf72017-08-12 22:56:47 +010083 :param test_obj: An instance of `tempest.test.BaseTestCase`.
84 :param toggle_rbac_role: Role to switch `os_primary` Tempest creds to.
85 :returns: None.
86 :raises RbacResourceSetupFailed: If admin or test roles are missing. Or
87 if `toggle_rbac_role` is not a boolean value or role validation
88 fails.
Felipe Monteiro7be94e82017-07-26 02:17:08 +010089 """
Felipe Monteiroe8d93e02017-07-19 20:52:20 +010090 self.user_id = test_obj.os_primary.credentials.user_id
91 self.project_id = test_obj.os_primary.credentials.tenant_id
92 self.token = test_obj.os_primary.auth_provider.get_token()
DavidPurcell029d8c32017-01-06 15:27:41 -050093
Felipe Monteiroe8d93e02017-07-19 20:52:20 +010094 LOG.debug('Switching role to: %s.', toggle_rbac_role)
Felipe Monteiro2693bf72017-08-12 22:56:47 +010095 role_already_present = False
96
DavidPurcell029d8c32017-01-06 15:27:41 -050097 try:
Felipe Monteiro2693bf72017-08-12 22:56:47 +010098 if not all([self.admin_role_id, self.rbac_role_id]):
99 self._get_roles_by_name()
DavidPurcell029d8c32017-01-06 15:27:41 -0500100
Felipe Monteirob35de582017-05-05 00:16:53 +0100101 self._validate_switch_role(test_obj, toggle_rbac_role)
DavidPurcell029d8c32017-01-06 15:27:41 -0500102
Felipe Monteiro2693bf72017-08-12 22:56:47 +0100103 target_role = (
104 self.rbac_role_id if toggle_rbac_role else self.admin_role_id)
105 role_already_present = self._list_and_clear_user_roles_on_project(
106 target_role)
107
108 # Do not switch roles if `target_role` already exists.
109 if not role_already_present:
110 self._create_user_role_on_project(target_role)
DavidPurcell029d8c32017-01-06 15:27:41 -0500111 except Exception as exp:
Felipe Monteiro2693bf72017-08-12 22:56:47 +0100112 with excutils.save_and_reraise_exception():
113 LOG.exception(exp)
Felipe Monteiro34a138c2017-03-02 17:01:37 -0500114 finally:
Felipe Monteiroe8d93e02017-07-19 20:52:20 +0100115 test_obj.os_primary.auth_provider.clear_auth()
Felipe Monteiro7be94e82017-07-26 02:17:08 +0100116 # Fernet tokens are not subsecond aware so sleep to ensure we are
117 # passing the second boundary before attempting to authenticate.
Felipe Monteiro2693bf72017-08-12 22:56:47 +0100118 # Only sleep if a token revocation occurred as a result of role
119 # switching. This will optimize test runtime in the case where
120 # ``[identity] admin_role`` == ``[rbac] rbac_test_role``.
121 if not role_already_present:
Rick Bartra89f498f2017-03-20 15:54:45 -0400122 time.sleep(1)
Felipe Monteiroe8d93e02017-07-19 20:52:20 +0100123 test_obj.os_primary.auth_provider.set_auth()
Felipe Monteiro34a138c2017-03-02 17:01:37 -0500124
Felipe Monteiro2693bf72017-08-12 22:56:47 +0100125 def _get_roles_by_name(self):
126 available_roles = self.admin_roles_client.list_roles()
127 admin_role_id = rbac_role_id = None
Felipe Monteirofa01d5f2017-04-01 06:18:25 +0100128
Felipe Monteiro2693bf72017-08-12 22:56:47 +0100129 for role in available_roles['roles']:
130 if role['name'] == CONF.rbac.rbac_test_role:
131 rbac_role_id = role['id']
132 if role['name'] == CONF.identity.admin_role:
133 admin_role_id = role['id']
134
135 if not all([admin_role_id, rbac_role_id]):
136 msg = ("Roles defined by `[rbac] rbac_test_role` and `[identity] "
137 "admin_role` must be defined in the system.")
138 raise rbac_exceptions.RbacResourceSetupFailed(msg)
139
140 self.admin_role_id = admin_role_id
141 self.rbac_role_id = rbac_role_id
142
143 def _create_user_role_on_project(self, role_id):
Felipe Monteiroe8d93e02017-07-19 20:52:20 +0100144 self.admin_roles_client.create_user_role_on_project(
Felipe Monteirofa01d5f2017-04-01 06:18:25 +0100145 self.project_id, self.user_id, role_id)
146
Felipe Monteiro2693bf72017-08-12 22:56:47 +0100147 def _list_and_clear_user_roles_on_project(self, role_id):
Felipe Monteiroe8d93e02017-07-19 20:52:20 +0100148 roles = self.admin_roles_client.list_user_roles_on_project(
Felipe Monteirofa01d5f2017-04-01 06:18:25 +0100149 self.project_id, self.user_id)['roles']
Felipe Monteirofa01d5f2017-04-01 06:18:25 +0100150 role_ids = [role['id'] for role in roles]
Felipe Monteiro2693bf72017-08-12 22:56:47 +0100151
152 # NOTE(felipemonteiro): We do not use ``role_id in role_ids`` here to
153 # avoid over-permission errors: if the current list of roles on the
154 # project includes "admin" and "Member", and we are switching to the
155 # "Member" role, then we must delete the "admin" role. Thus, we only
156 # return early if the user's roles on the project are an exact match.
157 if [role_id] == role_ids:
Felipe Monteirofa01d5f2017-04-01 06:18:25 +0100158 return True
Felipe Monteirob3b7bc82017-03-03 15:58:15 -0500159
160 for role in roles:
Felipe Monteiroe8d93e02017-07-19 20:52:20 +0100161 self.admin_roles_client.delete_role_from_user_on_project(
Felipe Monteirofa01d5f2017-04-01 06:18:25 +0100162 self.project_id, self.user_id, role['id'])
163
164 return False
165
Felipe Monteiro75f23632017-04-07 15:56:26 +0100166 def _validate_switch_role(self, test_obj, toggle_rbac_role):
Felipe Monteiro2693bf72017-08-12 22:56:47 +0100167 """Validates that the test role passed to `switch_role` is legal.
Felipe Monteirofa01d5f2017-04-01 06:18:25 +0100168
169 Throws an error for the following improper usages of `switch_role`:
170 * `switch_role` is not called with a boolean value
Felipe Monteiro2693bf72017-08-12 22:56:47 +0100171 * `switch_role` is never called inside a test, except in tearDown
Felipe Monteirofa01d5f2017-04-01 06:18:25 +0100172 * `switch_role` is called with the same boolean value twice
Felipe Monteiro7be94e82017-07-26 02:17:08 +0100173
174 If a `skipException` is thrown then this is a legitimate reason why
175 `switch_role` is not called.
Felipe Monteirofa01d5f2017-04-01 06:18:25 +0100176 """
Felipe Monteiro75f23632017-04-07 15:56:26 +0100177 if not isinstance(toggle_rbac_role, bool):
Felipe Monteirofa01d5f2017-04-01 06:18:25 +0100178 raise rbac_exceptions.RbacResourceSetupFailed(
Felipe Monteiro2693bf72017-08-12 22:56:47 +0100179 '`toggle_rbac_role` must be a boolean value.')
Felipe Monteirofa01d5f2017-04-01 06:18:25 +0100180
Felipe Monteiro521e5c12017-04-05 22:59:57 +0100181 # The unique key is the combination of module path plus class name.
182 class_name = test_obj.__name__ if isinstance(test_obj, type) else \
Felipe Monteirofa01d5f2017-04-01 06:18:25 +0100183 test_obj.__class__.__name__
Felipe Monteiro521e5c12017-04-05 22:59:57 +0100184 module_name = test_obj.__module__
185 key = '%s.%s' % (module_name, class_name)
186
Felipe Monteirofa01d5f2017-04-01 06:18:25 +0100187 self.switch_role_history.setdefault(key, None)
188
Felipe Monteiro75f23632017-04-07 15:56:26 +0100189 if self.switch_role_history[key] == toggle_rbac_role:
Felipe Monteiroba4881b2017-04-09 02:11:25 +0100190 # If an exception was thrown, like a skipException or otherwise,
191 # then this is a legitimate reason why `switch_role` was not
192 # called, so only raise an exception if no current exception is
193 # being handled.
194 if sys.exc_info()[0] is None:
Felipe Monteirofa01d5f2017-04-01 06:18:25 +0100195 self.switch_role_history[key] = False
Felipe Monteiro75f23632017-04-07 15:56:26 +0100196 error_message = '`toggle_rbac_role` must not be called with '\
Felipe Monteirofa01d5f2017-04-01 06:18:25 +0100197 'the same bool value twice. Make sure that you included '\
198 'a rbac_utils.switch_role method call inside the test.'
199 LOG.error(error_message)
200 raise rbac_exceptions.RbacResourceSetupFailed(error_message)
201 else:
Felipe Monteiro75f23632017-04-07 15:56:26 +0100202 self.switch_role_history[key] = toggle_rbac_role
Felipe Monteirofa01d5f2017-04-01 06:18:25 +0100203
Felipe Monteiro17e9b492017-05-27 05:45:20 +0100204
Felipe Monteiro8a043fb2017-08-06 06:29:05 +0100205def is_admin():
206 """Verifies whether the current test role equals the admin role.
207
208 :returns: True if ``rbac_test_role`` is the admin role.
209 """
210 return CONF.rbac.rbac_test_role == CONF.identity.admin_role
Rick Bartraed950052017-06-29 17:20:33 -0400211
212
213@six.add_metaclass(abc.ABCMeta)
214class RbacAuthority(object):
215 # TODO(rb560u): Add documentation explaining what this class is for
216
217 @abc.abstractmethod
218 def allowed(self, rule_name, role):
219 """Determine whether the role should be able to perform the API"""
220 return