blob: aff4e66d1401f4bbcc7e60bdd0c18c4b2d48da7e [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
Felipe Monteirob0595652017-01-23 16:51:58 -050016import copy
Felipe Monteiroae2ebab2017-03-23 22:49:06 +000017import json
DavidPurcell029d8c32017-01-06 15:27:41 -050018import os
19
DavidPurcell029d8c32017-01-06 15:27:41 -050020from oslo_log import log as logging
DavidPurcell029d8c32017-01-06 15:27:41 -050021from oslo_policy import policy
Felipe Monteiroae2ebab2017-03-23 22:49:06 +000022import stevedore
ghanshyam0df097d2017-08-08 09:28:17 +030023from tempest import clients
Rick Bartra503c5572017-03-09 13:49:58 -050024from tempest.common import credentials_factory as credentials
Felipe Monteiro7be94e82017-07-26 02:17:08 +010025from tempest import config
Rick Bartra503c5572017-03-09 13:49:58 -050026
Felipe Monteirob0595652017-01-23 16:51:58 -050027from patrole_tempest_plugin import rbac_exceptions
Rick Bartraed950052017-06-29 17:20:33 -040028from patrole_tempest_plugin.rbac_utils import RbacAuthority
DavidPurcell029d8c32017-01-06 15:27:41 -050029
Felipe Monteiro7be94e82017-07-26 02:17:08 +010030CONF = config.CONF
DavidPurcell029d8c32017-01-06 15:27:41 -050031LOG = logging.getLogger(__name__)
32
DavidPurcell029d8c32017-01-06 15:27:41 -050033
Rick Bartraed950052017-06-29 17:20:33 -040034class RbacPolicyParser(RbacAuthority):
DavidPurcell029d8c32017-01-06 15:27:41 -050035 """A class for parsing policy rules into lists of allowed roles.
36
37 RBAC testing requires that each rule in a policy file be broken up into
38 the roles that constitute it. This class automates that process.
Felipe Monteirob0595652017-01-23 16:51:58 -050039
40 The list of roles per rule can be reverse-engineered by checking, for
41 each role, whether a given rule is allowed using oslo policy.
DavidPurcell029d8c32017-01-06 15:27:41 -050042 """
43
Felipe Monteiro0854ded2017-05-05 16:30:55 +010044 def __init__(self, project_id, user_id, service, extra_target_data=None):
Felipe Monteiro322c5b62017-02-26 02:44:21 +000045 """Initialization of Rbac Policy Parser.
DavidPurcell029d8c32017-01-06 15:27:41 -050046
Felipe Monteiro9c978502017-01-27 17:07:54 -050047 Parses a policy file to create a dictionary, mapping policy actions to
48 roles. If a policy file does not exist, checks whether the policy file
49 is registered as a namespace under oslo.policy.policies. Nova, for
50 example, doesn't use a policy.json file by default; its policy is
51 implemented in code and registered as 'nova' under
52 oslo.policy.policies.
53
54 If the policy file is not found in either place, raises an exception.
55
56 Additionally, if the policy file exists in both code and as a
57 policy.json (for example, by creating a custom nova policy.json file),
58 the custom policy file over the default policy implementation is
59 prioritized.
Felipe Monteirob0595652017-01-23 16:51:58 -050060
Felipe Monteiro3ab2c352017-07-05 22:25:34 +010061 :param uuid project_id: project_id of object performing API call
62 :param uuid user_id: user_id of object performing API call
63 :param string service: service of the policy file
64 :param dict extra_target_data: dictionary containing additional object
65 data needed by oslo.policy to validate generic checks
DavidPurcell029d8c32017-01-06 15:27:41 -050066 """
Felipe Monteiroae2ebab2017-03-23 22:49:06 +000067
Felipe Monteiro0854ded2017-05-05 16:30:55 +010068 if extra_target_data is None:
69 extra_target_data = {}
70
Felipe Monteirod9607c42017-06-12 19:28:45 +010071 self.validate_service(service)
Rick Bartra503c5572017-03-09 13:49:58 -050072
Felipe Monteiro3ab2c352017-07-05 22:25:34 +010073 # Prioritize dynamically searching for policy files over relying on
74 # deprecated service-specific policy file locations.
Felipe Monteirof6eb8622017-08-06 06:08:02 +010075 if CONF.patrole.custom_policy_files:
Felipe Monteiro3ab2c352017-07-05 22:25:34 +010076 self.discover_policy_files()
77 self.path = self.policy_files.get(service)
78 else:
Felipe Monteirof6eb8622017-08-06 06:08:02 +010079 self.path = getattr(CONF.patrole, '%s_policy_file' % str(service),
Felipe Monteiro3ab2c352017-07-05 22:25:34 +010080 None)
81
Felipe Monteiroae2ebab2017-03-23 22:49:06 +000082 self.rules = policy.Rules.load(self._get_policy_data(service),
83 'default')
Felipe Monteirofd1db982017-04-13 21:19:41 +010084 self.project_id = project_id
Felipe Monteiro889264e2017-03-01 17:19:35 -050085 self.user_id = user_id
Felipe Monteirofd1db982017-04-13 21:19:41 +010086 self.extra_target_data = extra_target_data
DavidPurcell029d8c32017-01-06 15:27:41 -050087
Felipe Monteirod9607c42017-06-12 19:28:45 +010088 @classmethod
89 def validate_service(cls, service):
Felipe Monteiro3ab2c352017-07-05 22:25:34 +010090 """Validate whether the service passed to ``__init__`` exists."""
Felipe Monteirod9607c42017-06-12 19:28:45 +010091 service = service.lower().strip() if service else None
92
93 # Cache the list of available services in memory to avoid needlessly
94 # doing an API call every time.
95 if not hasattr(cls, 'available_services'):
ghanshyam0df097d2017-08-08 09:28:17 +030096 admin_mgr = clients.Manager(
97 credentials.get_configured_admin_credentials())
Felipe Monteiro7be94e82017-07-26 02:17:08 +010098 services_client = (admin_mgr.identity_services_v3_client
99 if CONF.identity_feature_enabled.api_v3
100 else admin_mgr.identity_services_client)
101 services = services_client.list_services()['services']
Felipe Monteirod9607c42017-06-12 19:28:45 +0100102 cls.available_services = [s['name'] for s in services]
103
104 if not service or service not in cls.available_services:
105 LOG.debug("%s is NOT a valid service.", service)
106 raise rbac_exceptions.RbacInvalidService(
107 "%s is NOT a valid service." % service)
108
Felipe Monteiro3ab2c352017-07-05 22:25:34 +0100109 @classmethod
110 def discover_policy_files(cls):
111 # Dynamically discover the policy file for each service in
112 # ``cls.available_services``. Pick the first ``candidate_path`` found
Felipe Monteirof6eb8622017-08-06 06:08:02 +0100113 # out of the potential paths in ``CONF.patrole.custom_policy_files``.
Felipe Monteiro3ab2c352017-07-05 22:25:34 +0100114 if not hasattr(cls, 'policy_files'):
115 cls.policy_files = {}
116 for service in cls.available_services:
Felipe Monteirof6eb8622017-08-06 06:08:02 +0100117 for candidate_path in CONF.patrole.custom_policy_files:
Felipe Monteiro3ab2c352017-07-05 22:25:34 +0100118 if os.path.isfile(candidate_path % service):
119 cls.policy_files.setdefault(service,
120 candidate_path % service)
121
Felipe Monteirob0595652017-01-23 16:51:58 -0500122 def allowed(self, rule_name, role):
Felipe Monteiro9c978502017-01-27 17:07:54 -0500123 is_admin_context = self._is_admin_context(role)
Felipe Monteirob0595652017-01-23 16:51:58 -0500124 is_allowed = self._allowed(
Felipe Monteiro9c978502017-01-27 17:07:54 -0500125 access=self._get_access_token(role),
Felipe Monteirob0595652017-01-23 16:51:58 -0500126 apply_rule=rule_name,
Felipe Monteiro9c978502017-01-27 17:07:54 -0500127 is_admin=is_admin_context)
Felipe Monteiro9c978502017-01-27 17:07:54 -0500128 return is_allowed
DavidPurcell029d8c32017-01-06 15:27:41 -0500129
Felipe Monteiroae2ebab2017-03-23 22:49:06 +0000130 def _get_policy_data(self, service):
131 file_policy_data = {}
132 mgr_policy_data = {}
133 policy_data = {}
134
Felipe Monteiro3ab2c352017-07-05 22:25:34 +0100135 # Check whether policy file exists and attempt to read it.
136 if self.path and os.path.isfile(self.path):
Felipe Monteiroae2ebab2017-03-23 22:49:06 +0000137 try:
Samantha Blanco85f79d72017-04-21 11:09:14 -0400138 with open(self.path, 'r') as policy_file:
139 file_policy_data = policy_file.read()
Felipe Monteiroae2ebab2017-03-23 22:49:06 +0000140 file_policy_data = json.loads(file_policy_data)
Samantha Blanco85f79d72017-04-21 11:09:14 -0400141 except (IOError, ValueError) as e:
142 msg = "Failed to read policy file for service. "
143 if isinstance(e, IOError):
144 msg += "Please check that policy path exists."
145 else:
146 msg += "JSON may be improperly formatted."
147 LOG.debug(msg)
148 file_policy_data = {}
Felipe Monteiroae2ebab2017-03-23 22:49:06 +0000149
150 # Check whether policy actions are defined in code. Nova and Keystone,
151 # for example, define their default policy actions in code.
152 mgr = stevedore.named.NamedExtensionManager(
153 'oslo.policy.policies',
154 names=[service],
155 on_load_failure_callback=None,
156 invoke_on_load=True,
157 warn_on_missing_entrypoint=False)
158
159 if mgr:
160 policy_generator = {policy.name: policy.obj for policy in mgr}
161 if policy_generator and service in policy_generator:
162 for rule in policy_generator[service]:
163 mgr_policy_data[rule.name] = str(rule.check)
164
165 # If data from both file and code exist, combine both together.
166 if file_policy_data and mgr_policy_data:
167 # Add the policy actions from code first.
168 for action, rule in mgr_policy_data.items():
169 policy_data[action] = rule
170 # Overwrite with any custom policy actions defined in policy.json.
171 for action, rule in file_policy_data.items():
172 policy_data[action] = rule
173 elif file_policy_data:
174 policy_data = file_policy_data
175 elif mgr_policy_data:
176 policy_data = mgr_policy_data
177 else:
Felipe Monteiro3ab2c352017-07-05 22:25:34 +0100178 error_message = (
179 'Policy file for {0} service neither found in code nor at {1}.'
180 .format(service, [loc % service for loc in
Felipe Monteirof6eb8622017-08-06 06:08:02 +0100181 CONF.patrole.custom_policy_files])
Felipe Monteiro3ab2c352017-07-05 22:25:34 +0100182 )
Felipe Monteiroae2ebab2017-03-23 22:49:06 +0000183 raise rbac_exceptions.RbacParsingException(error_message)
184
185 try:
186 policy_data = json.dumps(policy_data)
187 except ValueError:
188 error_message = 'Policy file for {0} service is invalid.'.format(
189 service)
190 raise rbac_exceptions.RbacParsingException(error_message)
191
192 return policy_data
193
Felipe Monteiro9c978502017-01-27 17:07:54 -0500194 def _is_admin_context(self, role):
195 """Checks whether a role has admin context.
196
197 If context_is_admin is contained in the policy file, then checks
198 whether the given role is contained in context_is_admin. If it is not
199 in the policy file, then default to context_is_admin: admin.
200 """
201 if 'context_is_admin' in self.rules.keys():
202 return self._allowed(
203 access=self._get_access_token(role),
204 apply_rule='context_is_admin')
Felipe Monteirof6b69e22017-05-04 21:55:04 +0100205 return role == CONF.identity.admin_role
DavidPurcell029d8c32017-01-06 15:27:41 -0500206
Felipe Monteirob0595652017-01-23 16:51:58 -0500207 def _get_access_token(self, role):
208 access_token = {
209 "token": {
210 "roles": [
211 {
212 "name": role
213 }
214 ],
Felipe Monteirofd1db982017-04-13 21:19:41 +0100215 "project_id": self.project_id,
216 "tenant_id": self.project_id,
Felipe Monteiro889264e2017-03-01 17:19:35 -0500217 "user_id": self.user_id
Felipe Monteirob0595652017-01-23 16:51:58 -0500218 }
219 }
220 return access_token
DavidPurcell029d8c32017-01-06 15:27:41 -0500221
Felipe Monteiro9c978502017-01-27 17:07:54 -0500222 def _allowed(self, access, apply_rule, is_admin=False):
Felipe Monteirob0595652017-01-23 16:51:58 -0500223 """Checks if a given rule in a policy is allowed with given access.
DavidPurcell029d8c32017-01-06 15:27:41 -0500224
Felipe Monteirob0595652017-01-23 16:51:58 -0500225 Adapted from oslo_policy.shell.
DavidPurcell029d8c32017-01-06 15:27:41 -0500226
Felipe Monteirob0595652017-01-23 16:51:58 -0500227 :param access: type dict: dictionary from ``_get_access_token``
228 :param apply_rule: type string: rule to be checked
229 :param is_admin: type bool: whether admin context is used
DavidPurcell029d8c32017-01-06 15:27:41 -0500230 """
Felipe Monteirob0595652017-01-23 16:51:58 -0500231 access_data = copy.copy(access['token'])
232 access_data['roles'] = [role['name'] for role in access_data['roles']]
Felipe Monteirob0595652017-01-23 16:51:58 -0500233 access_data['is_admin'] = is_admin
Felipe Monteiro9c978502017-01-27 17:07:54 -0500234 # TODO(felipemonteiro): Dynamically calculate is_admin_project rather
235 # than hard-coding it to True. is_admin_project cannot be determined
236 # from the role, but rather from project and domain names. See
237 # _populate_is_admin_project in keystone.token.providers.common
238 # for more information.
239 access_data['is_admin_project'] = True
DavidPurcell029d8c32017-01-06 15:27:41 -0500240
Felipe Monteirob0595652017-01-23 16:51:58 -0500241 class Object(object):
242 pass
243 o = Object()
Felipe Monteiro9c978502017-01-27 17:07:54 -0500244 o.rules = self.rules
DavidPurcell029d8c32017-01-06 15:27:41 -0500245
Felipe Monteiro9fc782e2017-02-01 15:38:46 -0500246 target = {"project_id": access_data['project_id'],
247 "tenant_id": access_data['project_id'],
Felipe Monteiro889264e2017-03-01 17:19:35 -0500248 "network:tenant_id": access_data['project_id'],
249 "user_id": access_data['user_id']}
Felipe Monteirofd1db982017-04-13 21:19:41 +0100250 if self.extra_target_data:
251 target.update(self.extra_target_data)
Felipe Monteirob0595652017-01-23 16:51:58 -0500252
Felipe Monteiro9c978502017-01-27 17:07:54 -0500253 result = self._try_rule(apply_rule, target, access_data, o)
Felipe Monteirob0595652017-01-23 16:51:58 -0500254 return result
255
Felipe Monteiro9c978502017-01-27 17:07:54 -0500256 def _try_rule(self, apply_rule, target, access_data, o):
Samantha Blanco0d880082017-03-23 18:14:37 -0400257 if apply_rule not in self.rules:
Felipe Monteiro48c913d2017-03-15 12:07:48 -0400258 message = "Policy action: {0} not found in policy file: {1}."\
259 .format(apply_rule, self.path)
260 LOG.debug(message)
261 raise rbac_exceptions.RbacParsingException(message)
Samantha Blanco0d880082017-03-23 18:14:37 -0400262 else:
263 rule = self.rules[apply_rule]
264 return rule(target, access_data, o)