blob: 9fd8aee99462be6b7d5e98d6ddfad2ac83300449 [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 Monteirofa01d5f2017-04-01 06:18:25 +010022import oslo_utils.uuidutils as uuid_utils
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
42 defined by ``CONF.identity.admin_role`` and `CONF.rbac.rbac_test_role``.
43 """
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
48 :param test_obj: A Tempest test instance.
49 :type test_obj: tempest.lib.base.BaseTestCase or
50 tempest.test.BaseTestCase
51 """
52 # Since we are going to instantiate a client manager with
53 # admin credentials, first check if admin is available.
54 if not credentials.is_admin_available(
55 identity_version=test_obj.get_identity_version()):
56 msg = "Missing Identity Admin API credentials in configuration."
57 raise testtools.TestCase.skipException(msg)
58
59 # Intialize the admin roles_client to perform role switching.
60 admin_creds = test_obj.get_client_manager(credential_type='admin')
61 if test_obj.get_identity_version() == 'v3':
62 admin_roles_client = admin_creds.roles_v3_client
63 else:
64 admin_roles_client = admin_creds.roles_client
65
66 self.admin_roles_client = admin_roles_client
Felipe Monteirob35de582017-05-05 00:16:53 +010067 self.switch_role(test_obj, toggle_rbac_role=False)
68
Felipe Monteiro75f23632017-04-07 15:56:26 +010069 # References the last value of `toggle_rbac_role` that was passed to
Felipe Monteirofa01d5f2017-04-01 06:18:25 +010070 # `switch_role`. Used for ensuring that `switch_role` is correctly used
71 # in a test file, so that false positives are prevented. The key used
Felipe Monteiro521e5c12017-04-05 22:59:57 +010072 # to index into the dictionary is the module path plus class name, which is
73 # unique.
Felipe Monteirofa01d5f2017-04-01 06:18:25 +010074 switch_role_history = {}
75 admin_role_id = None
76 rbac_role_id = None
DavidPurcell029d8c32017-01-06 15:27:41 -050077
Felipe Monteiro75f23632017-04-07 15:56:26 +010078 def switch_role(self, test_obj, toggle_rbac_role=False):
Felipe Monteiroe8d93e02017-07-19 20:52:20 +010079 self.user_id = test_obj.os_primary.credentials.user_id
80 self.project_id = test_obj.os_primary.credentials.tenant_id
81 self.token = test_obj.os_primary.auth_provider.get_token()
DavidPurcell029d8c32017-01-06 15:27:41 -050082
Felipe Monteiroe8d93e02017-07-19 20:52:20 +010083 LOG.debug('Switching role to: %s.', toggle_rbac_role)
DavidPurcell029d8c32017-01-06 15:27:41 -050084 try:
Felipe Monteirofa01d5f2017-04-01 06:18:25 +010085 if not self.admin_role_id or not self.rbac_role_id:
86 self._get_roles()
DavidPurcell029d8c32017-01-06 15:27:41 -050087
Felipe Monteirob35de582017-05-05 00:16:53 +010088 self._validate_switch_role(test_obj, toggle_rbac_role)
DavidPurcell029d8c32017-01-06 15:27:41 -050089
Felipe Monteiro75f23632017-04-07 15:56:26 +010090 if toggle_rbac_role:
Felipe Monteirofa01d5f2017-04-01 06:18:25 +010091 self._add_role_to_user(self.rbac_role_id)
DavidPurcell029d8c32017-01-06 15:27:41 -050092 else:
Felipe Monteirofa01d5f2017-04-01 06:18:25 +010093 self._add_role_to_user(self.admin_role_id)
DavidPurcell029d8c32017-01-06 15:27:41 -050094 except Exception as exp:
95 LOG.error(exp)
96 raise
Felipe Monteiro34a138c2017-03-02 17:01:37 -050097 finally:
Felipe Monteiro23923f02017-03-17 02:15:07 +000098 # NOTE(felipemonteiro): These two comments below are copied from
99 # tempest.api.identity.v2/v3.test_users.
100 #
101 # Reset auth again to verify the password restore does work.
102 # Clear auth restores the original credentials and deletes
103 # cached auth data.
Felipe Monteiroe8d93e02017-07-19 20:52:20 +0100104 test_obj.os_primary.auth_provider.clear_auth()
Felipe Monteiro23923f02017-03-17 02:15:07 +0000105 # Fernet tokens are not subsecond aware and Keystone should only be
106 # precise to the second. Sleep to ensure we are passing the second
Rick Bartra89f498f2017-03-20 15:54:45 -0400107 # boundary before attempting to authenticate. If token is of type
108 # uuid, then do not sleep.
Felipe Monteirofa01d5f2017-04-01 06:18:25 +0100109 if not uuid_utils.is_uuid_like(self.token):
Rick Bartra89f498f2017-03-20 15:54:45 -0400110 time.sleep(1)
Felipe Monteiroe8d93e02017-07-19 20:52:20 +0100111 test_obj.os_primary.auth_provider.set_auth()
Felipe Monteiro34a138c2017-03-02 17:01:37 -0500112
Felipe Monteirofa01d5f2017-04-01 06:18:25 +0100113 def _add_role_to_user(self, role_id):
114 role_already_present = self._clear_user_roles(role_id)
115 if role_already_present:
116 return
117
Felipe Monteiroe8d93e02017-07-19 20:52:20 +0100118 self.admin_roles_client.create_user_role_on_project(
Felipe Monteirofa01d5f2017-04-01 06:18:25 +0100119 self.project_id, self.user_id, role_id)
120
121 def _clear_user_roles(self, role_id):
Felipe Monteiroe8d93e02017-07-19 20:52:20 +0100122 roles = self.admin_roles_client.list_user_roles_on_project(
Felipe Monteirofa01d5f2017-04-01 06:18:25 +0100123 self.project_id, self.user_id)['roles']
124
125 # If the user already has the role that is required, return early.
126 role_ids = [role['id'] for role in roles]
127 if role_ids == [role_id]:
128 return True
Felipe Monteirob3b7bc82017-03-03 15:58:15 -0500129
130 for role in roles:
Felipe Monteiroe8d93e02017-07-19 20:52:20 +0100131 self.admin_roles_client.delete_role_from_user_on_project(
Felipe Monteirofa01d5f2017-04-01 06:18:25 +0100132 self.project_id, self.user_id, role['id'])
133
134 return False
135
Felipe Monteiro75f23632017-04-07 15:56:26 +0100136 def _validate_switch_role(self, test_obj, toggle_rbac_role):
Felipe Monteirofa01d5f2017-04-01 06:18:25 +0100137 """Validates that the rbac role passed to `switch_role` is legal.
138
139 Throws an error for the following improper usages of `switch_role`:
140 * `switch_role` is not called with a boolean value
141 * `switch_role` is never called in a test file, except in tearDown
142 * `switch_role` is called with the same boolean value twice
143 """
Felipe Monteiro75f23632017-04-07 15:56:26 +0100144 if not isinstance(toggle_rbac_role, bool):
Felipe Monteirofa01d5f2017-04-01 06:18:25 +0100145 raise rbac_exceptions.RbacResourceSetupFailed(
Felipe Monteiro75f23632017-04-07 15:56:26 +0100146 'toggle_rbac_role must be a boolean value.')
Felipe Monteirofa01d5f2017-04-01 06:18:25 +0100147
Felipe Monteiro521e5c12017-04-05 22:59:57 +0100148 # The unique key is the combination of module path plus class name.
149 class_name = test_obj.__name__ if isinstance(test_obj, type) else \
Felipe Monteirofa01d5f2017-04-01 06:18:25 +0100150 test_obj.__class__.__name__
Felipe Monteiro521e5c12017-04-05 22:59:57 +0100151 module_name = test_obj.__module__
152 key = '%s.%s' % (module_name, class_name)
153
Felipe Monteirofa01d5f2017-04-01 06:18:25 +0100154 self.switch_role_history.setdefault(key, None)
155
Felipe Monteiro75f23632017-04-07 15:56:26 +0100156 if self.switch_role_history[key] == toggle_rbac_role:
Felipe Monteiroba4881b2017-04-09 02:11:25 +0100157 # If an exception was thrown, like a skipException or otherwise,
158 # then this is a legitimate reason why `switch_role` was not
159 # called, so only raise an exception if no current exception is
160 # being handled.
161 if sys.exc_info()[0] is None:
Felipe Monteirofa01d5f2017-04-01 06:18:25 +0100162 self.switch_role_history[key] = False
Felipe Monteiro75f23632017-04-07 15:56:26 +0100163 error_message = '`toggle_rbac_role` must not be called with '\
Felipe Monteirofa01d5f2017-04-01 06:18:25 +0100164 'the same bool value twice. Make sure that you included '\
165 'a rbac_utils.switch_role method call inside the test.'
166 LOG.error(error_message)
167 raise rbac_exceptions.RbacResourceSetupFailed(error_message)
168 else:
Felipe Monteiro75f23632017-04-07 15:56:26 +0100169 self.switch_role_history[key] = toggle_rbac_role
Felipe Monteirofa01d5f2017-04-01 06:18:25 +0100170
171 def _get_roles(self):
Felipe Monteiroe8d93e02017-07-19 20:52:20 +0100172 available_roles = self.admin_roles_client.list_roles()
Felipe Monteirofa01d5f2017-04-01 06:18:25 +0100173 admin_role_id = rbac_role_id = None
174
175 for role in available_roles['roles']:
176 if role['name'] == CONF.rbac.rbac_test_role:
177 rbac_role_id = role['id']
Felipe Monteirof6b69e22017-05-04 21:55:04 +0100178 if role['name'] == CONF.identity.admin_role:
Felipe Monteirofa01d5f2017-04-01 06:18:25 +0100179 admin_role_id = role['id']
180
181 if not admin_role_id or not rbac_role_id:
182 msg = "Role with name 'admin' does not exist in the system."\
183 if not admin_role_id else "Role defined by rbac_test_role "\
184 "does not exist in the system."
185 raise rbac_exceptions.RbacResourceSetupFailed(msg)
186
187 self.admin_role_id = admin_role_id
188 self.rbac_role_id = rbac_role_id
Felipe Monteiro17e9b492017-05-27 05:45:20 +0100189
190 @property
191 def is_admin(self):
192 """Verifies whether the current test role equals the admin role.
193
194 :returns: True if ``rbac_test_role`` is the admin role.
195 """
196 return CONF.rbac.rbac_test_role == CONF.identity.admin_role
Rick Bartraed950052017-06-29 17:20:33 -0400197
198
199@six.add_metaclass(abc.ABCMeta)
200class RbacAuthority(object):
201 # TODO(rb560u): Add documentation explaining what this class is for
202
203 @abc.abstractmethod
204 def allowed(self, rule_name, role):
205 """Determine whether the role should be able to perform the API"""
206 return