blob: 347f77fbd4752e5adcd8bb25b20f8cb6744d078d [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
Rick Bartraed950052017-06-29 17:20:33 -040018import six
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 Monteiro10e82fd2017-11-21 01:47:20 +000035 """Utility class responsible for switching ``os_primary`` role.
Felipe Monteiroe8d93e02017-07-19 20:52:20 +010036
37 This class is responsible for overriding the value of the primary Tempest
Felipe Monteiro10e82fd2017-11-21 01:47:20 +000038 credential's role (i.e. ``os_primary`` role). By doing so, it is possible
39 to seamlessly swap between admin credentials, needed for setup and clean
40 up, and primary credentials, needed to perform the API call which does
Felipe Monteiroe8d93e02017-07-19 20:52:20 +010041 policy enforcement. The primary credentials always cycle between roles
Felipe Monteiro10e82fd2017-11-21 01:47:20 +000042 defined by ``CONF.identity.admin_role`` and
43 ``CONF.patrole.rbac_test_role``.
Felipe Monteiroe8d93e02017-07-19 20:52:20 +010044 """
DavidPurcell029d8c32017-01-06 15:27:41 -050045
Felipe Monteirob35de582017-05-05 00:16:53 +010046 def __init__(self, test_obj):
Felipe Monteiroe8d93e02017-07-19 20:52:20 +010047 """Constructor for ``RbacUtils``.
48
Felipe Monteiro2693bf72017-08-12 22:56:47 +010049 :param test_obj: An instance of `tempest.test.BaseTestCase`.
Felipe Monteiroe8d93e02017-07-19 20:52:20 +010050 """
Felipe Monteiroe8d93e02017-07-19 20:52:20 +010051 # Intialize the admin roles_client to perform role switching.
Felipe Monteiro3e14f472017-08-17 23:02:11 +010052 admin_mgr = clients.Manager(
53 credentials.get_configured_admin_credentials())
Felipe Monteiroe8d93e02017-07-19 20:52:20 +010054 if test_obj.get_identity_version() == 'v3':
Felipe Monteiro3e14f472017-08-17 23:02:11 +010055 admin_roles_client = admin_mgr.roles_v3_client
Felipe Monteiroe8d93e02017-07-19 20:52:20 +010056 else:
Felipe Monteiro3e14f472017-08-17 23:02:11 +010057 admin_roles_client = admin_mgr.roles_client
Felipe Monteiroe8d93e02017-07-19 20:52:20 +010058
59 self.admin_roles_client = admin_roles_client
Felipe Monteiro10e82fd2017-11-21 01:47:20 +000060 self._override_role(test_obj, False)
Felipe Monteirob35de582017-05-05 00:16:53 +010061
Felipe Monteirofa01d5f2017-04-01 06:18:25 +010062 admin_role_id = None
63 rbac_role_id = None
DavidPurcell029d8c32017-01-06 15:27:41 -050064
Felipe Monteiro10e82fd2017-11-21 01:47:20 +000065 @contextmanager
66 def override_role(self, test_obj):
67 """Override the role used by ``os_primary`` Tempest credentials.
68
69 Temporarily change the role used by ``os_primary`` credentials to:
Masayuki Igawa80b9aab2018-01-09 17:00:45 +090070
71 * ``[patrole] rbac_test_role`` before test execution
72 * ``[identity] admin_role`` after test execution
Felipe Monteiro10e82fd2017-11-21 01:47:20 +000073
74 Automatically switches to admin role after test execution.
75
76 :param test_obj: Instance of ``tempest.test.BaseTestCase``.
77 :returns: None
78
79 .. warning::
80
81 This function can alter user roles for pre-provisioned credentials.
82 Work is underway to safely clean up after this function.
83
84 Example::
85
86 @rbac_rule_validation.action(service='test',
87 rule='a:test:rule')
88 def test_foo(self):
89 # Allocate test-level resources here.
90 with self.rbac_utils.override_role(self):
melissaml7cd21612018-05-23 21:00:50 +080091 # The role for `os_primary` has now been overridden. Within
Felipe Monteiro10e82fd2017-11-21 01:47:20 +000092 # this block, call the API endpoint that enforces the
93 # expected policy specified by "rule" in the decorator.
94 self.foo_service.bar_api_call()
95 # The role is switched back to admin automatically. Note that
96 # if the API call above threw an exception, any code below this
97 # point in the test is not executed.
98 """
99 self._override_role(test_obj, True)
100 try:
101 # Execute the test.
102 yield
103 finally:
104 # This code block is always executed, no matter the result of the
105 # test. Automatically switch back to the admin role for test clean
106 # up.
107 self._override_role(test_obj, False)
108
Felipe Monteiro10e82fd2017-11-21 01:47:20 +0000109 def _override_role(self, test_obj, toggle_rbac_role=False):
110 """Private helper for overriding ``os_primary`` Tempest credentials.
111
Felipe Monteiro07a1c172017-12-10 04:26:08 +0000112 :param test_obj: instance of :py:class:`tempest.test.BaseTestCase`
Felipe Monteiro10e82fd2017-11-21 01:47:20 +0000113 :param toggle_rbac_role: Boolean value that controls the role that
114 overrides default role of ``os_primary`` credentials.
115 * If True: role is set to ``[patrole] rbac_test_role``
116 * If False: role is set to ``[identity] admin_role``
117 """
Felipe Monteiroe8d93e02017-07-19 20:52:20 +0100118 self.user_id = test_obj.os_primary.credentials.user_id
119 self.project_id = test_obj.os_primary.credentials.tenant_id
120 self.token = test_obj.os_primary.auth_provider.get_token()
DavidPurcell029d8c32017-01-06 15:27:41 -0500121
Felipe Monteiro10e82fd2017-11-21 01:47:20 +0000122 LOG.debug('Overriding role to: %s.', toggle_rbac_role)
Felipe Monteiro2693bf72017-08-12 22:56:47 +0100123 role_already_present = False
124
DavidPurcell029d8c32017-01-06 15:27:41 -0500125 try:
Felipe Monteiro2693bf72017-08-12 22:56:47 +0100126 if not all([self.admin_role_id, self.rbac_role_id]):
127 self._get_roles_by_name()
DavidPurcell029d8c32017-01-06 15:27:41 -0500128
Felipe Monteiro2693bf72017-08-12 22:56:47 +0100129 target_role = (
130 self.rbac_role_id if toggle_rbac_role else self.admin_role_id)
131 role_already_present = self._list_and_clear_user_roles_on_project(
132 target_role)
133
Felipe Monteiro10e82fd2017-11-21 01:47:20 +0000134 # Do not override roles if `target_role` already exists.
Felipe Monteiro2693bf72017-08-12 22:56:47 +0100135 if not role_already_present:
136 self._create_user_role_on_project(target_role)
DavidPurcell029d8c32017-01-06 15:27:41 -0500137 except Exception as exp:
Felipe Monteiro2693bf72017-08-12 22:56:47 +0100138 with excutils.save_and_reraise_exception():
139 LOG.exception(exp)
Felipe Monteiro34a138c2017-03-02 17:01:37 -0500140 finally:
Felipe Monteiroe8d93e02017-07-19 20:52:20 +0100141 test_obj.os_primary.auth_provider.clear_auth()
Felipe Monteiro7be94e82017-07-26 02:17:08 +0100142 # Fernet tokens are not subsecond aware so sleep to ensure we are
143 # passing the second boundary before attempting to authenticate.
Felipe Monteiro2693bf72017-08-12 22:56:47 +0100144 # Only sleep if a token revocation occurred as a result of role
Felipe Monteiro10e82fd2017-11-21 01:47:20 +0000145 # overriding. This will optimize test runtime in the case where
Felipe Monteirob58c1192017-11-20 01:50:24 +0000146 # ``[identity] admin_role`` == ``[patrole] rbac_test_role``.
Felipe Monteiro2693bf72017-08-12 22:56:47 +0100147 if not role_already_present:
Rick Bartra89f498f2017-03-20 15:54:45 -0400148 time.sleep(1)
Felipe Monteiroe8d93e02017-07-19 20:52:20 +0100149 test_obj.os_primary.auth_provider.set_auth()
Felipe Monteiro34a138c2017-03-02 17:01:37 -0500150
Felipe Monteiro2693bf72017-08-12 22:56:47 +0100151 def _get_roles_by_name(self):
152 available_roles = self.admin_roles_client.list_roles()
153 admin_role_id = rbac_role_id = None
Felipe Monteirofa01d5f2017-04-01 06:18:25 +0100154
Felipe Monteiro2693bf72017-08-12 22:56:47 +0100155 for role in available_roles['roles']:
Felipe Monteirof6eb8622017-08-06 06:08:02 +0100156 if role['name'] == CONF.patrole.rbac_test_role:
Felipe Monteiro2693bf72017-08-12 22:56:47 +0100157 rbac_role_id = role['id']
158 if role['name'] == CONF.identity.admin_role:
159 admin_role_id = role['id']
160
161 if not all([admin_role_id, rbac_role_id]):
Felipe Monteirof6eb8622017-08-06 06:08:02 +0100162 msg = ("Roles defined by `[patrole] rbac_test_role` and "
163 "`[identity] admin_role` must be defined in the system.")
Felipe Monteiro2693bf72017-08-12 22:56:47 +0100164 raise rbac_exceptions.RbacResourceSetupFailed(msg)
165
166 self.admin_role_id = admin_role_id
167 self.rbac_role_id = rbac_role_id
168
169 def _create_user_role_on_project(self, role_id):
Felipe Monteiroe8d93e02017-07-19 20:52:20 +0100170 self.admin_roles_client.create_user_role_on_project(
Felipe Monteirofa01d5f2017-04-01 06:18:25 +0100171 self.project_id, self.user_id, role_id)
172
Felipe Monteiro2693bf72017-08-12 22:56:47 +0100173 def _list_and_clear_user_roles_on_project(self, role_id):
Felipe Monteiroe8d93e02017-07-19 20:52:20 +0100174 roles = self.admin_roles_client.list_user_roles_on_project(
Felipe Monteirofa01d5f2017-04-01 06:18:25 +0100175 self.project_id, self.user_id)['roles']
Felipe Monteirofa01d5f2017-04-01 06:18:25 +0100176 role_ids = [role['id'] for role in roles]
Felipe Monteiro2693bf72017-08-12 22:56:47 +0100177
178 # NOTE(felipemonteiro): We do not use ``role_id in role_ids`` here to
179 # avoid over-permission errors: if the current list of roles on the
180 # project includes "admin" and "Member", and we are switching to the
181 # "Member" role, then we must delete the "admin" role. Thus, we only
182 # return early if the user's roles on the project are an exact match.
183 if [role_id] == role_ids:
Felipe Monteirofa01d5f2017-04-01 06:18:25 +0100184 return True
Felipe Monteirob3b7bc82017-03-03 15:58:15 -0500185
186 for role in roles:
Felipe Monteiroe8d93e02017-07-19 20:52:20 +0100187 self.admin_roles_client.delete_role_from_user_on_project(
Felipe Monteirofa01d5f2017-04-01 06:18:25 +0100188 self.project_id, self.user_id, role['id'])
189
190 return False
191
Felipe Monteiro17e9b492017-05-27 05:45:20 +0100192
Felipe Monteiro07a1c172017-12-10 04:26:08 +0000193class RbacUtilsMixin(object):
194 """Mixin class to be used alongside an instance of
195 :py:class:`tempest.test.BaseTestCase`.
196
197 Should be used to perform Patrole class setup for a base RBAC class. Child
198 classes should not use this mixin.
199
200 Example::
201
202 class BaseRbacTest(rbac_utils.RbacUtilsMixin, base.BaseV2ComputeTest):
203
204 @classmethod
205 def skip_checks(cls):
206 super(BaseRbacTest, cls).skip_checks()
207 cls.skip_rbac_checks()
208
209 @classmethod
210 def setup_clients(cls):
211 super(BaseRbacTest, cls).setup_clients()
212 cls.setup_rbac_utils()
213 """
214
215 @classmethod
216 def skip_rbac_checks(cls):
217 if not CONF.patrole.enable_rbac:
218 raise cls.skipException(
Felipe Monteirod7371992018-04-25 16:57:09 +0100219 'Patrole testing not enabled so skipping %s.' % cls.__name__)
Felipe Monteiro07a1c172017-12-10 04:26:08 +0000220
221 @classmethod
222 def setup_rbac_utils(cls):
223 cls.rbac_utils = RbacUtils(cls)
224
225
Felipe Monteiro8a043fb2017-08-06 06:29:05 +0100226def is_admin():
227 """Verifies whether the current test role equals the admin role.
228
229 :returns: True if ``rbac_test_role`` is the admin role.
230 """
Felipe Monteirof6eb8622017-08-06 06:08:02 +0100231 return CONF.patrole.rbac_test_role == CONF.identity.admin_role
Rick Bartraed950052017-06-29 17:20:33 -0400232
233
234@six.add_metaclass(abc.ABCMeta)
235class RbacAuthority(object):
Felipe Monteiro2a3b5132017-08-11 01:19:06 +0100236 """Class for validating whether a given role can perform a policy action.
237
238 Any class that extends ``RbacAuthority`` provides the logic for determining
239 whether a role has permissions to execute a policy action.
240 """
Rick Bartraed950052017-06-29 17:20:33 -0400241
242 @abc.abstractmethod
Felipe Monteiro2a3b5132017-08-11 01:19:06 +0100243 def allowed(self, rule, role):
244 """Determine whether the role should be able to perform the API.
245
246 :param rule: The name of the policy enforced by the API.
247 :param role: The role used to determine whether ``rule`` can be
248 executed.
249 :returns: True if the ``role`` has permissions to execute
250 ``rule``, else False.
251 """