blob: 49cb5e1fa5b197d388f0c62f1c6e5f187231db29 [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
Felipe Monteiro10e82fd2017-11-21 01:47:20 +000017from contextlib import contextmanager
18import debtcollector.removals
Rick Bartraed950052017-06-29 17:20:33 -040019import six
DavidPurcell029d8c32017-01-06 15:27:41 -050020import time
Felipe Monteiro34a138c2017-03-02 17:01:37 -050021
Rajiv Kumar645dfc92017-01-19 13:48:27 +053022from oslo_log import log as logging
Felipe Monteiro2693bf72017-08-12 22:56:47 +010023from oslo_utils import excutils
Felipe Monteirofa01d5f2017-04-01 06:18:25 +010024
Felipe Monteiro3e14f472017-08-17 23:02:11 +010025from tempest import clients
Felipe Monteiroe7e552e2017-05-02 17:04:12 +010026from tempest.common import credentials_factory as credentials
Felipe Monteirofa01d5f2017-04-01 06:18:25 +010027from tempest import config
DavidPurcell029d8c32017-01-06 15:27:41 -050028
Felipe Monteiro34a138c2017-03-02 17:01:37 -050029from patrole_tempest_plugin import rbac_exceptions
DavidPurcell029d8c32017-01-06 15:27:41 -050030
DavidPurcell029d8c32017-01-06 15:27:41 -050031CONF = config.CONF
Felipe Monteiro34a138c2017-03-02 17:01:37 -050032LOG = logging.getLogger(__name__)
DavidPurcell029d8c32017-01-06 15:27:41 -050033
34
DavidPurcell029d8c32017-01-06 15:27:41 -050035class RbacUtils(object):
Felipe Monteiro10e82fd2017-11-21 01:47:20 +000036 """Utility class responsible for switching ``os_primary`` role.
Felipe Monteiroe8d93e02017-07-19 20:52:20 +010037
38 This class is responsible for overriding the value of the primary Tempest
Felipe Monteiro10e82fd2017-11-21 01:47:20 +000039 credential's role (i.e. ``os_primary`` role). By doing so, it is possible
40 to seamlessly swap between admin credentials, needed for setup and clean
41 up, and primary credentials, needed to perform the API call which does
Felipe Monteiroe8d93e02017-07-19 20:52:20 +010042 policy enforcement. The primary credentials always cycle between roles
Felipe Monteiro10e82fd2017-11-21 01:47:20 +000043 defined by ``CONF.identity.admin_role`` and
44 ``CONF.patrole.rbac_test_role``.
Felipe Monteiroe8d93e02017-07-19 20:52:20 +010045 """
DavidPurcell029d8c32017-01-06 15:27:41 -050046
Felipe Monteirob35de582017-05-05 00:16:53 +010047 def __init__(self, test_obj):
Felipe Monteiroe8d93e02017-07-19 20:52:20 +010048 """Constructor for ``RbacUtils``.
49
Felipe Monteiro2693bf72017-08-12 22:56:47 +010050 :param test_obj: An instance of `tempest.test.BaseTestCase`.
Felipe Monteiroe8d93e02017-07-19 20:52:20 +010051 """
Felipe Monteiroe8d93e02017-07-19 20:52:20 +010052 # Intialize the admin roles_client to perform role switching.
Felipe Monteiro3e14f472017-08-17 23:02:11 +010053 admin_mgr = clients.Manager(
54 credentials.get_configured_admin_credentials())
Felipe Monteiroe8d93e02017-07-19 20:52:20 +010055 if test_obj.get_identity_version() == 'v3':
Felipe Monteiro3e14f472017-08-17 23:02:11 +010056 admin_roles_client = admin_mgr.roles_v3_client
Felipe Monteiroe8d93e02017-07-19 20:52:20 +010057 else:
Felipe Monteiro3e14f472017-08-17 23:02:11 +010058 admin_roles_client = admin_mgr.roles_client
Felipe Monteiroe8d93e02017-07-19 20:52:20 +010059
60 self.admin_roles_client = admin_roles_client
Felipe Monteiro10e82fd2017-11-21 01:47:20 +000061 self._override_role(test_obj, False)
Felipe Monteirob35de582017-05-05 00:16:53 +010062
Felipe Monteirofa01d5f2017-04-01 06:18:25 +010063 admin_role_id = None
64 rbac_role_id = None
DavidPurcell029d8c32017-01-06 15:27:41 -050065
Felipe Monteiro10e82fd2017-11-21 01:47:20 +000066 @contextmanager
67 def override_role(self, test_obj):
68 """Override the role used by ``os_primary`` Tempest credentials.
69
70 Temporarily change the role used by ``os_primary`` credentials to:
Masayuki Igawa80b9aab2018-01-09 17:00:45 +090071
72 * ``[patrole] rbac_test_role`` before test execution
73 * ``[identity] admin_role`` after test execution
Felipe Monteiro10e82fd2017-11-21 01:47:20 +000074
75 Automatically switches to admin role after test execution.
76
77 :param test_obj: Instance of ``tempest.test.BaseTestCase``.
78 :returns: None
79
80 .. warning::
81
82 This function can alter user roles for pre-provisioned credentials.
83 Work is underway to safely clean up after this function.
84
85 Example::
86
87 @rbac_rule_validation.action(service='test',
88 rule='a:test:rule')
89 def test_foo(self):
90 # Allocate test-level resources here.
91 with self.rbac_utils.override_role(self):
92 # The role for `os_primary` has now been overriden. Within
93 # this block, call the API endpoint that enforces the
94 # expected policy specified by "rule" in the decorator.
95 self.foo_service.bar_api_call()
96 # The role is switched back to admin automatically. Note that
97 # if the API call above threw an exception, any code below this
98 # point in the test is not executed.
99 """
100 self._override_role(test_obj, True)
101 try:
102 # Execute the test.
103 yield
104 finally:
105 # This code block is always executed, no matter the result of the
106 # test. Automatically switch back to the admin role for test clean
107 # up.
108 self._override_role(test_obj, False)
109
110 @debtcollector.removals.remove(removal_version='Rocky')
111 def switch_role(self, test_obj, toggle_rbac_role):
Felipe Monteiro7be94e82017-07-26 02:17:08 +0100112 """Switch the role used by `os_primary` Tempest credentials.
113
114 Switch the role used by `os_primary` credentials to:
Masayuki Igawa80b9aab2018-01-09 17:00:45 +0900115
116 * admin if `toggle_rbac_role` is False
117 * `CONF.patrole.rbac_test_role` if `toggle_rbac_role` is True
Felipe Monteiro7be94e82017-07-26 02:17:08 +0100118
Felipe Monteiro07a1c172017-12-10 04:26:08 +0000119 :param test_obj: instance of :py:class:`tempest.test.BaseTestCase`
Felipe Monteirof6eb8622017-08-06 06:08:02 +0100120 :param toggle_rbac_role: role to switch `os_primary` Tempest creds to
Felipe Monteiro7be94e82017-07-26 02:17:08 +0100121 """
Felipe Monteiro10e82fd2017-11-21 01:47:20 +0000122 self._override_role(test_obj, toggle_rbac_role)
123
124 def _override_role(self, test_obj, toggle_rbac_role=False):
125 """Private helper for overriding ``os_primary`` Tempest credentials.
126
Felipe Monteiro07a1c172017-12-10 04:26:08 +0000127 :param test_obj: instance of :py:class:`tempest.test.BaseTestCase`
Felipe Monteiro10e82fd2017-11-21 01:47:20 +0000128 :param toggle_rbac_role: Boolean value that controls the role that
129 overrides default role of ``os_primary`` credentials.
130 * If True: role is set to ``[patrole] rbac_test_role``
131 * If False: role is set to ``[identity] admin_role``
132 """
Felipe Monteiroe8d93e02017-07-19 20:52:20 +0100133 self.user_id = test_obj.os_primary.credentials.user_id
134 self.project_id = test_obj.os_primary.credentials.tenant_id
135 self.token = test_obj.os_primary.auth_provider.get_token()
DavidPurcell029d8c32017-01-06 15:27:41 -0500136
Felipe Monteiro10e82fd2017-11-21 01:47:20 +0000137 LOG.debug('Overriding role to: %s.', toggle_rbac_role)
Felipe Monteiro2693bf72017-08-12 22:56:47 +0100138 role_already_present = False
139
DavidPurcell029d8c32017-01-06 15:27:41 -0500140 try:
Felipe Monteiro2693bf72017-08-12 22:56:47 +0100141 if not all([self.admin_role_id, self.rbac_role_id]):
142 self._get_roles_by_name()
DavidPurcell029d8c32017-01-06 15:27:41 -0500143
Felipe Monteiro2693bf72017-08-12 22:56:47 +0100144 target_role = (
145 self.rbac_role_id if toggle_rbac_role else self.admin_role_id)
146 role_already_present = self._list_and_clear_user_roles_on_project(
147 target_role)
148
Felipe Monteiro10e82fd2017-11-21 01:47:20 +0000149 # Do not override roles if `target_role` already exists.
Felipe Monteiro2693bf72017-08-12 22:56:47 +0100150 if not role_already_present:
151 self._create_user_role_on_project(target_role)
DavidPurcell029d8c32017-01-06 15:27:41 -0500152 except Exception as exp:
Felipe Monteiro2693bf72017-08-12 22:56:47 +0100153 with excutils.save_and_reraise_exception():
154 LOG.exception(exp)
Felipe Monteiro34a138c2017-03-02 17:01:37 -0500155 finally:
Felipe Monteiroe8d93e02017-07-19 20:52:20 +0100156 test_obj.os_primary.auth_provider.clear_auth()
Felipe Monteiro7be94e82017-07-26 02:17:08 +0100157 # Fernet tokens are not subsecond aware so sleep to ensure we are
158 # passing the second boundary before attempting to authenticate.
Felipe Monteiro2693bf72017-08-12 22:56:47 +0100159 # Only sleep if a token revocation occurred as a result of role
Felipe Monteiro10e82fd2017-11-21 01:47:20 +0000160 # overriding. This will optimize test runtime in the case where
Felipe Monteirob58c1192017-11-20 01:50:24 +0000161 # ``[identity] admin_role`` == ``[patrole] rbac_test_role``.
Felipe Monteiro2693bf72017-08-12 22:56:47 +0100162 if not role_already_present:
Rick Bartra89f498f2017-03-20 15:54:45 -0400163 time.sleep(1)
Felipe Monteiroe8d93e02017-07-19 20:52:20 +0100164 test_obj.os_primary.auth_provider.set_auth()
Felipe Monteiro34a138c2017-03-02 17:01:37 -0500165
Felipe Monteiro2693bf72017-08-12 22:56:47 +0100166 def _get_roles_by_name(self):
167 available_roles = self.admin_roles_client.list_roles()
168 admin_role_id = rbac_role_id = None
Felipe Monteirofa01d5f2017-04-01 06:18:25 +0100169
Felipe Monteiro2693bf72017-08-12 22:56:47 +0100170 for role in available_roles['roles']:
Felipe Monteirof6eb8622017-08-06 06:08:02 +0100171 if role['name'] == CONF.patrole.rbac_test_role:
Felipe Monteiro2693bf72017-08-12 22:56:47 +0100172 rbac_role_id = role['id']
173 if role['name'] == CONF.identity.admin_role:
174 admin_role_id = role['id']
175
176 if not all([admin_role_id, rbac_role_id]):
Felipe Monteirof6eb8622017-08-06 06:08:02 +0100177 msg = ("Roles defined by `[patrole] rbac_test_role` and "
178 "`[identity] admin_role` must be defined in the system.")
Felipe Monteiro2693bf72017-08-12 22:56:47 +0100179 raise rbac_exceptions.RbacResourceSetupFailed(msg)
180
181 self.admin_role_id = admin_role_id
182 self.rbac_role_id = rbac_role_id
183
184 def _create_user_role_on_project(self, role_id):
Felipe Monteiroe8d93e02017-07-19 20:52:20 +0100185 self.admin_roles_client.create_user_role_on_project(
Felipe Monteirofa01d5f2017-04-01 06:18:25 +0100186 self.project_id, self.user_id, role_id)
187
Felipe Monteiro2693bf72017-08-12 22:56:47 +0100188 def _list_and_clear_user_roles_on_project(self, role_id):
Felipe Monteiroe8d93e02017-07-19 20:52:20 +0100189 roles = self.admin_roles_client.list_user_roles_on_project(
Felipe Monteirofa01d5f2017-04-01 06:18:25 +0100190 self.project_id, self.user_id)['roles']
Felipe Monteirofa01d5f2017-04-01 06:18:25 +0100191 role_ids = [role['id'] for role in roles]
Felipe Monteiro2693bf72017-08-12 22:56:47 +0100192
193 # NOTE(felipemonteiro): We do not use ``role_id in role_ids`` here to
194 # avoid over-permission errors: if the current list of roles on the
195 # project includes "admin" and "Member", and we are switching to the
196 # "Member" role, then we must delete the "admin" role. Thus, we only
197 # return early if the user's roles on the project are an exact match.
198 if [role_id] == role_ids:
Felipe Monteirofa01d5f2017-04-01 06:18:25 +0100199 return True
Felipe Monteirob3b7bc82017-03-03 15:58:15 -0500200
201 for role in roles:
Felipe Monteiroe8d93e02017-07-19 20:52:20 +0100202 self.admin_roles_client.delete_role_from_user_on_project(
Felipe Monteirofa01d5f2017-04-01 06:18:25 +0100203 self.project_id, self.user_id, role['id'])
204
205 return False
206
Felipe Monteiro17e9b492017-05-27 05:45:20 +0100207
Felipe Monteiro07a1c172017-12-10 04:26:08 +0000208class RbacUtilsMixin(object):
209 """Mixin class to be used alongside an instance of
210 :py:class:`tempest.test.BaseTestCase`.
211
212 Should be used to perform Patrole class setup for a base RBAC class. Child
213 classes should not use this mixin.
214
215 Example::
216
217 class BaseRbacTest(rbac_utils.RbacUtilsMixin, base.BaseV2ComputeTest):
218
219 @classmethod
220 def skip_checks(cls):
221 super(BaseRbacTest, cls).skip_checks()
222 cls.skip_rbac_checks()
223
224 @classmethod
225 def setup_clients(cls):
226 super(BaseRbacTest, cls).setup_clients()
227 cls.setup_rbac_utils()
228 """
229
230 @classmethod
231 def skip_rbac_checks(cls):
232 if not CONF.patrole.enable_rbac:
233 raise cls.skipException(
234 '%s skipped as Patrole testing not enabled.' % cls.__name__)
235
236 @classmethod
237 def setup_rbac_utils(cls):
238 cls.rbac_utils = RbacUtils(cls)
239
240
Felipe Monteiro8a043fb2017-08-06 06:29:05 +0100241def is_admin():
242 """Verifies whether the current test role equals the admin role.
243
244 :returns: True if ``rbac_test_role`` is the admin role.
245 """
Felipe Monteirof6eb8622017-08-06 06:08:02 +0100246 return CONF.patrole.rbac_test_role == CONF.identity.admin_role
Rick Bartraed950052017-06-29 17:20:33 -0400247
248
249@six.add_metaclass(abc.ABCMeta)
250class RbacAuthority(object):
Felipe Monteiro2a3b5132017-08-11 01:19:06 +0100251 """Class for validating whether a given role can perform a policy action.
252
253 Any class that extends ``RbacAuthority`` provides the logic for determining
254 whether a role has permissions to execute a policy action.
255 """
Rick Bartraed950052017-06-29 17:20:33 -0400256
257 @abc.abstractmethod
Felipe Monteiro2a3b5132017-08-11 01:19:06 +0100258 def allowed(self, rule, role):
259 """Determine whether the role should be able to perform the API.
260
261 :param rule: The name of the policy enforced by the API.
262 :param role: The role used to determine whether ``rule`` can be
263 executed.
264 :returns: True if the ``role`` has permissions to execute
265 ``rule``, else False.
266 """