blob: 17a626c25c17e4f8defcb4b030db3a19751d726b [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
Felipe Monteiroae2ebab2017-03-23 22:49:06 +000020from oslo_config import cfg
DavidPurcell029d8c32017-01-06 15:27:41 -050021from oslo_log import log as logging
DavidPurcell029d8c32017-01-06 15:27:41 -050022from oslo_policy import policy
Felipe Monteiroae2ebab2017-03-23 22:49:06 +000023import stevedore
DavidPurcell029d8c32017-01-06 15:27:41 -050024
Rick Bartra503c5572017-03-09 13:49:58 -050025from tempest.common import credentials_factory as credentials
26
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 Monteiroae2ebab2017-03-23 22:49:06 +000030CONF = cfg.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 Monteirofd1db982017-04-13 21:19:41 +010061 :param project_id: type uuid
Felipe Monteiro889264e2017-03-01 17:19:35 -050062 :param user_id: type uuid
DavidPurcell029d8c32017-01-06 15:27:41 -050063 :param service: type string
64 :param path: type string
65 """
Felipe Monteiroae2ebab2017-03-23 22:49:06 +000066
Felipe Monteiro0854ded2017-05-05 16:30:55 +010067 if extra_target_data is None:
68 extra_target_data = {}
69
Felipe Monteirod9607c42017-06-12 19:28:45 +010070 # First check if the service is valid.
71 self.validate_service(service)
Rick Bartra503c5572017-03-09 13:49:58 -050072
Felipe Monteiroae2ebab2017-03-23 22:49:06 +000073 # Use default path in /etc/<service_name/policy.json if no path
74 # is provided.
Samantha Blanco85f79d72017-04-21 11:09:14 -040075 path = getattr(CONF.rbac, '%s_policy_file' % str(service), None)
76 if not path:
77 LOG.info("No config option found for %s,"
Felipe Monteiro4bf66a22017-05-07 14:44:21 +010078 " using default path", str(service))
Samantha Blanco85f79d72017-04-21 11:09:14 -040079 path = os.path.join('/etc', service, 'policy.json')
80 self.path = path
Felipe Monteiroae2ebab2017-03-23 22:49:06 +000081 self.rules = policy.Rules.load(self._get_policy_data(service),
82 'default')
Felipe Monteirofd1db982017-04-13 21:19:41 +010083 self.project_id = project_id
Felipe Monteiro889264e2017-03-01 17:19:35 -050084 self.user_id = user_id
Felipe Monteirofd1db982017-04-13 21:19:41 +010085 self.extra_target_data = extra_target_data
DavidPurcell029d8c32017-01-06 15:27:41 -050086
Felipe Monteirod9607c42017-06-12 19:28:45 +010087 @classmethod
88 def validate_service(cls, service):
89 """Validate whether the service passed to ``init`` exists."""
90 service = service.lower().strip() if service else None
91
92 # Cache the list of available services in memory to avoid needlessly
93 # doing an API call every time.
94 if not hasattr(cls, 'available_services'):
95 admin_mgr = credentials.AdminManager()
96 services = admin_mgr.identity_services_v3_client.\
97 list_services()['services']
98 cls.available_services = [s['name'] for s in services]
99
100 if not service or service not in cls.available_services:
101 LOG.debug("%s is NOT a valid service.", service)
102 raise rbac_exceptions.RbacInvalidService(
103 "%s is NOT a valid service." % service)
104
Felipe Monteirob0595652017-01-23 16:51:58 -0500105 def allowed(self, rule_name, role):
Felipe Monteiro9c978502017-01-27 17:07:54 -0500106 is_admin_context = self._is_admin_context(role)
Felipe Monteirob0595652017-01-23 16:51:58 -0500107 is_allowed = self._allowed(
Felipe Monteiro9c978502017-01-27 17:07:54 -0500108 access=self._get_access_token(role),
Felipe Monteirob0595652017-01-23 16:51:58 -0500109 apply_rule=rule_name,
Felipe Monteiro9c978502017-01-27 17:07:54 -0500110 is_admin=is_admin_context)
Felipe Monteiro9c978502017-01-27 17:07:54 -0500111 return is_allowed
DavidPurcell029d8c32017-01-06 15:27:41 -0500112
Felipe Monteiroae2ebab2017-03-23 22:49:06 +0000113 def _get_policy_data(self, service):
114 file_policy_data = {}
115 mgr_policy_data = {}
116 policy_data = {}
117
118 # Check whether policy file exists.
119 if os.path.isfile(self.path):
Felipe Monteiroae2ebab2017-03-23 22:49:06 +0000120 try:
Samantha Blanco85f79d72017-04-21 11:09:14 -0400121 with open(self.path, 'r') as policy_file:
122 file_policy_data = policy_file.read()
Felipe Monteiroae2ebab2017-03-23 22:49:06 +0000123 file_policy_data = json.loads(file_policy_data)
Samantha Blanco85f79d72017-04-21 11:09:14 -0400124 except (IOError, ValueError) as e:
125 msg = "Failed to read policy file for service. "
126 if isinstance(e, IOError):
127 msg += "Please check that policy path exists."
128 else:
129 msg += "JSON may be improperly formatted."
130 LOG.debug(msg)
131 file_policy_data = {}
Felipe Monteiroae2ebab2017-03-23 22:49:06 +0000132
133 # Check whether policy actions are defined in code. Nova and Keystone,
134 # for example, define their default policy actions in code.
135 mgr = stevedore.named.NamedExtensionManager(
136 'oslo.policy.policies',
137 names=[service],
138 on_load_failure_callback=None,
139 invoke_on_load=True,
140 warn_on_missing_entrypoint=False)
141
142 if mgr:
143 policy_generator = {policy.name: policy.obj for policy in mgr}
144 if policy_generator and service in policy_generator:
145 for rule in policy_generator[service]:
146 mgr_policy_data[rule.name] = str(rule.check)
147
148 # If data from both file and code exist, combine both together.
149 if file_policy_data and mgr_policy_data:
150 # Add the policy actions from code first.
151 for action, rule in mgr_policy_data.items():
152 policy_data[action] = rule
153 # Overwrite with any custom policy actions defined in policy.json.
154 for action, rule in file_policy_data.items():
155 policy_data[action] = rule
156 elif file_policy_data:
157 policy_data = file_policy_data
158 elif mgr_policy_data:
159 policy_data = mgr_policy_data
160 else:
161 error_message = 'Policy file for {0} service neither found in '\
162 'code nor at {1}.'.format(service, self.path)
163 raise rbac_exceptions.RbacParsingException(error_message)
164
165 try:
166 policy_data = json.dumps(policy_data)
167 except ValueError:
168 error_message = 'Policy file for {0} service is invalid.'.format(
169 service)
170 raise rbac_exceptions.RbacParsingException(error_message)
171
172 return policy_data
173
Felipe Monteiro9c978502017-01-27 17:07:54 -0500174 def _is_admin_context(self, role):
175 """Checks whether a role has admin context.
176
177 If context_is_admin is contained in the policy file, then checks
178 whether the given role is contained in context_is_admin. If it is not
179 in the policy file, then default to context_is_admin: admin.
180 """
181 if 'context_is_admin' in self.rules.keys():
182 return self._allowed(
183 access=self._get_access_token(role),
184 apply_rule='context_is_admin')
Felipe Monteirof6b69e22017-05-04 21:55:04 +0100185 return role == CONF.identity.admin_role
DavidPurcell029d8c32017-01-06 15:27:41 -0500186
Felipe Monteirob0595652017-01-23 16:51:58 -0500187 def _get_access_token(self, role):
188 access_token = {
189 "token": {
190 "roles": [
191 {
192 "name": role
193 }
194 ],
Felipe Monteirofd1db982017-04-13 21:19:41 +0100195 "project_id": self.project_id,
196 "tenant_id": self.project_id,
Felipe Monteiro889264e2017-03-01 17:19:35 -0500197 "user_id": self.user_id
Felipe Monteirob0595652017-01-23 16:51:58 -0500198 }
199 }
200 return access_token
DavidPurcell029d8c32017-01-06 15:27:41 -0500201
Felipe Monteiro9c978502017-01-27 17:07:54 -0500202 def _allowed(self, access, apply_rule, is_admin=False):
Felipe Monteirob0595652017-01-23 16:51:58 -0500203 """Checks if a given rule in a policy is allowed with given access.
DavidPurcell029d8c32017-01-06 15:27:41 -0500204
Felipe Monteirob0595652017-01-23 16:51:58 -0500205 Adapted from oslo_policy.shell.
DavidPurcell029d8c32017-01-06 15:27:41 -0500206
Felipe Monteirob0595652017-01-23 16:51:58 -0500207 :param access: type dict: dictionary from ``_get_access_token``
208 :param apply_rule: type string: rule to be checked
209 :param is_admin: type bool: whether admin context is used
DavidPurcell029d8c32017-01-06 15:27:41 -0500210 """
Felipe Monteirob0595652017-01-23 16:51:58 -0500211 access_data = copy.copy(access['token'])
212 access_data['roles'] = [role['name'] for role in access_data['roles']]
Felipe Monteirob0595652017-01-23 16:51:58 -0500213 access_data['is_admin'] = is_admin
Felipe Monteiro9c978502017-01-27 17:07:54 -0500214 # TODO(felipemonteiro): Dynamically calculate is_admin_project rather
215 # than hard-coding it to True. is_admin_project cannot be determined
216 # from the role, but rather from project and domain names. See
217 # _populate_is_admin_project in keystone.token.providers.common
218 # for more information.
219 access_data['is_admin_project'] = True
DavidPurcell029d8c32017-01-06 15:27:41 -0500220
Felipe Monteirob0595652017-01-23 16:51:58 -0500221 class Object(object):
222 pass
223 o = Object()
Felipe Monteiro9c978502017-01-27 17:07:54 -0500224 o.rules = self.rules
DavidPurcell029d8c32017-01-06 15:27:41 -0500225
Felipe Monteiro9fc782e2017-02-01 15:38:46 -0500226 target = {"project_id": access_data['project_id'],
227 "tenant_id": access_data['project_id'],
Felipe Monteiro889264e2017-03-01 17:19:35 -0500228 "network:tenant_id": access_data['project_id'],
229 "user_id": access_data['user_id']}
Felipe Monteirofd1db982017-04-13 21:19:41 +0100230 if self.extra_target_data:
231 target.update(self.extra_target_data)
Felipe Monteirob0595652017-01-23 16:51:58 -0500232
Felipe Monteiro9c978502017-01-27 17:07:54 -0500233 result = self._try_rule(apply_rule, target, access_data, o)
Felipe Monteirob0595652017-01-23 16:51:58 -0500234 return result
235
Felipe Monteiro9c978502017-01-27 17:07:54 -0500236 def _try_rule(self, apply_rule, target, access_data, o):
Samantha Blanco0d880082017-03-23 18:14:37 -0400237 if apply_rule not in self.rules:
Felipe Monteiro48c913d2017-03-15 12:07:48 -0400238 message = "Policy action: {0} not found in policy file: {1}."\
239 .format(apply_rule, self.path)
240 LOG.debug(message)
241 raise rbac_exceptions.RbacParsingException(message)
Samantha Blanco0d880082017-03-23 18:14:37 -0400242 else:
243 rule = self.rules[apply_rule]
244 return rule(target, access_data, o)