blob: 6851942559d01b3e178b2c05c675acd79e281b0c [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
Felipe Monteiro88a5bab2017-08-31 04:00:32 +010034class PolicyAuthority(RbacAuthority):
Felipe Monteirof2b58d72017-08-31 22:40:36 +010035 """A class that uses ``oslo.policy`` for validating RBAC."""
DavidPurcell029d8c32017-01-06 15:27:41 -050036
Felipe Monteiro0854ded2017-05-05 16:30:55 +010037 def __init__(self, project_id, user_id, service, extra_target_data=None):
Felipe Monteirof2b58d72017-08-31 22:40:36 +010038 """Initialization of Policy Authority class.
DavidPurcell029d8c32017-01-06 15:27:41 -050039
Felipe Monteirof2b58d72017-08-31 22:40:36 +010040 Validates whether a test role can perform a policy action by querying
41 ``oslo.policy`` with necessary test data.
Felipe Monteiro9c978502017-01-27 17:07:54 -050042
Felipe Monteirof2b58d72017-08-31 22:40:36 +010043 If a policy file does not exist, checks whether the policy file is
44 registered as a namespace under "oslo.policy.policies". Nova, for
45 example, doesn't use a policy file by default; its policies are
46 implemented in code and registered as "nova" under
47 "oslo.policy.policies".
Felipe Monteiro9c978502017-01-27 17:07:54 -050048
Felipe Monteirof2b58d72017-08-31 22:40:36 +010049 If the policy file is not found in either code or in a policy file,
50 then an exception is raised.
51
52 Additionally, if a custom policy file exists along with the default
53 policy in code implementation, the custom policy is prioritized.
Felipe Monteirob0595652017-01-23 16:51:58 -050054
Felipe Monteiro3ab2c352017-07-05 22:25:34 +010055 :param uuid project_id: project_id of object performing API call
56 :param uuid user_id: user_id of object performing API call
57 :param string service: service of the policy file
58 :param dict extra_target_data: dictionary containing additional object
59 data needed by oslo.policy to validate generic checks
Felipe Monteirof2b58d72017-08-31 22:40:36 +010060
61 Example:
62
63 .. code-block:: python
64
65 # Below is the default policy implementation in code, defined in
66 # a service like Nova.
67 test_policies = [
68 policy.DocumentedRuleDefault(
69 'service:test_rule',
70 base.RULE_ADMIN_OR_OWNER,
71 "This is a description for a test policy",
72 [
73 {
74 'method': 'POST',
75 'path': '/path/to/test/resource'
76 }
77 ]),
78 'service:another_test_rule',
79 base.RULE_ADMIN_OR_OWNER,
80 "This is a description for another test policy",
81 [
82 {
83 'method': 'GET',
84 'path': '/path/to/test/resource'
85 }
86 ]),
87 ]
88
89 .. code-block:: yaml
90
91 # Below is the custom override of the default policy in a YAML
92 # policy file. Note that the default rule is "rule:admin_or_owner"
93 # and the custom rule is "rule:admin_api". The `PolicyAuthority`
94 # class will use the "rule:admin_api" definition for this policy
95 # action.
96 "service:test_rule" : "rule:admin_api"
97
98 # Note below that no override is provided for
99 # "service:another_test_rule", which means that the default policy
100 # rule is used: "rule:admin_or_owner".
DavidPurcell029d8c32017-01-06 15:27:41 -0500101 """
Felipe Monteiroae2ebab2017-03-23 22:49:06 +0000102
Felipe Monteiro0854ded2017-05-05 16:30:55 +0100103 if extra_target_data is None:
104 extra_target_data = {}
105
Felipe Monteirod9607c42017-06-12 19:28:45 +0100106 self.validate_service(service)
Rick Bartra503c5572017-03-09 13:49:58 -0500107
Felipe Monteiro3ab2c352017-07-05 22:25:34 +0100108 # Prioritize dynamically searching for policy files over relying on
109 # deprecated service-specific policy file locations.
Felipe Monteirobbd6a3c2017-11-01 01:57:49 +0000110 self.path = None
Felipe Monteirof6eb8622017-08-06 06:08:02 +0100111 if CONF.patrole.custom_policy_files:
Felipe Monteiro3ab2c352017-07-05 22:25:34 +0100112 self.discover_policy_files()
113 self.path = self.policy_files.get(service)
Felipe Monteiro3ab2c352017-07-05 22:25:34 +0100114
Felipe Monteiroae2ebab2017-03-23 22:49:06 +0000115 self.rules = policy.Rules.load(self._get_policy_data(service),
116 'default')
Felipe Monteirofd1db982017-04-13 21:19:41 +0100117 self.project_id = project_id
Felipe Monteiro889264e2017-03-01 17:19:35 -0500118 self.user_id = user_id
Felipe Monteirofd1db982017-04-13 21:19:41 +0100119 self.extra_target_data = extra_target_data
DavidPurcell029d8c32017-01-06 15:27:41 -0500120
Felipe Monteirod9607c42017-06-12 19:28:45 +0100121 @classmethod
122 def validate_service(cls, service):
Felipe Monteiro3ab2c352017-07-05 22:25:34 +0100123 """Validate whether the service passed to ``__init__`` exists."""
Felipe Monteirod9607c42017-06-12 19:28:45 +0100124 service = service.lower().strip() if service else None
125
126 # Cache the list of available services in memory to avoid needlessly
127 # doing an API call every time.
128 if not hasattr(cls, 'available_services'):
ghanshyam0df097d2017-08-08 09:28:17 +0300129 admin_mgr = clients.Manager(
130 credentials.get_configured_admin_credentials())
Felipe Monteiro7be94e82017-07-26 02:17:08 +0100131 services_client = (admin_mgr.identity_services_v3_client
132 if CONF.identity_feature_enabled.api_v3
133 else admin_mgr.identity_services_client)
134 services = services_client.list_services()['services']
Felipe Monteirod9607c42017-06-12 19:28:45 +0100135 cls.available_services = [s['name'] for s in services]
136
137 if not service or service not in cls.available_services:
138 LOG.debug("%s is NOT a valid service.", service)
139 raise rbac_exceptions.RbacInvalidService(
140 "%s is NOT a valid service." % service)
141
Felipe Monteiro3ab2c352017-07-05 22:25:34 +0100142 @classmethod
143 def discover_policy_files(cls):
Felipe Monteirof2b58d72017-08-31 22:40:36 +0100144 """Dynamically discover the policy file for each service in
145 ``cls.available_services``. Pick the first candidate path found
146 out of the potential paths in ``[patrole] custom_policy_files``.
147 """
Felipe Monteiro3ab2c352017-07-05 22:25:34 +0100148 if not hasattr(cls, 'policy_files'):
149 cls.policy_files = {}
150 for service in cls.available_services:
Felipe Monteirof6eb8622017-08-06 06:08:02 +0100151 for candidate_path in CONF.patrole.custom_policy_files:
Felipe Monteiro3ab2c352017-07-05 22:25:34 +0100152 if os.path.isfile(candidate_path % service):
153 cls.policy_files.setdefault(service,
154 candidate_path % service)
155
Felipe Monteirob0595652017-01-23 16:51:58 -0500156 def allowed(self, rule_name, role):
Felipe Monteirof2b58d72017-08-31 22:40:36 +0100157 """Checks if a given rule in a policy is allowed with given role.
158
159 :param string rule_name: Rule to be checked using ``oslo.policy``.
160 :param bool is_admin: Whether admin context is used.
161 """
Felipe Monteiro9c978502017-01-27 17:07:54 -0500162 is_admin_context = self._is_admin_context(role)
Felipe Monteirob0595652017-01-23 16:51:58 -0500163 is_allowed = self._allowed(
Felipe Monteiro9c978502017-01-27 17:07:54 -0500164 access=self._get_access_token(role),
Felipe Monteirob0595652017-01-23 16:51:58 -0500165 apply_rule=rule_name,
Felipe Monteiro9c978502017-01-27 17:07:54 -0500166 is_admin=is_admin_context)
Felipe Monteiro9c978502017-01-27 17:07:54 -0500167 return is_allowed
DavidPurcell029d8c32017-01-06 15:27:41 -0500168
Felipe Monteiroae2ebab2017-03-23 22:49:06 +0000169 def _get_policy_data(self, service):
170 file_policy_data = {}
171 mgr_policy_data = {}
172 policy_data = {}
173
Felipe Monteiro3ab2c352017-07-05 22:25:34 +0100174 # Check whether policy file exists and attempt to read it.
175 if self.path and os.path.isfile(self.path):
Felipe Monteiroae2ebab2017-03-23 22:49:06 +0000176 try:
Samantha Blanco85f79d72017-04-21 11:09:14 -0400177 with open(self.path, 'r') as policy_file:
178 file_policy_data = policy_file.read()
Felipe Monteiroae2ebab2017-03-23 22:49:06 +0000179 file_policy_data = json.loads(file_policy_data)
Samantha Blanco85f79d72017-04-21 11:09:14 -0400180 except (IOError, ValueError) as e:
181 msg = "Failed to read policy file for service. "
182 if isinstance(e, IOError):
183 msg += "Please check that policy path exists."
184 else:
185 msg += "JSON may be improperly formatted."
186 LOG.debug(msg)
187 file_policy_data = {}
Felipe Monteiroae2ebab2017-03-23 22:49:06 +0000188
189 # Check whether policy actions are defined in code. Nova and Keystone,
190 # for example, define their default policy actions in code.
191 mgr = stevedore.named.NamedExtensionManager(
192 'oslo.policy.policies',
193 names=[service],
194 on_load_failure_callback=None,
195 invoke_on_load=True,
196 warn_on_missing_entrypoint=False)
197
198 if mgr:
199 policy_generator = {policy.name: policy.obj for policy in mgr}
200 if policy_generator and service in policy_generator:
201 for rule in policy_generator[service]:
202 mgr_policy_data[rule.name] = str(rule.check)
203
204 # If data from both file and code exist, combine both together.
205 if file_policy_data and mgr_policy_data:
206 # Add the policy actions from code first.
207 for action, rule in mgr_policy_data.items():
208 policy_data[action] = rule
209 # Overwrite with any custom policy actions defined in policy.json.
210 for action, rule in file_policy_data.items():
211 policy_data[action] = rule
212 elif file_policy_data:
213 policy_data = file_policy_data
214 elif mgr_policy_data:
215 policy_data = mgr_policy_data
216 else:
Felipe Monteiro3ab2c352017-07-05 22:25:34 +0100217 error_message = (
218 'Policy file for {0} service neither found in code nor at {1}.'
219 .format(service, [loc % service for loc in
Felipe Monteirof6eb8622017-08-06 06:08:02 +0100220 CONF.patrole.custom_policy_files])
Felipe Monteiro3ab2c352017-07-05 22:25:34 +0100221 )
Felipe Monteiroae2ebab2017-03-23 22:49:06 +0000222 raise rbac_exceptions.RbacParsingException(error_message)
223
224 try:
225 policy_data = json.dumps(policy_data)
Felipe Monteirob5809632017-10-26 04:12:12 +0100226 except (TypeError, ValueError):
Felipe Monteiroae2ebab2017-03-23 22:49:06 +0000227 error_message = 'Policy file for {0} service is invalid.'.format(
228 service)
229 raise rbac_exceptions.RbacParsingException(error_message)
230
231 return policy_data
232
Felipe Monteiro9c978502017-01-27 17:07:54 -0500233 def _is_admin_context(self, role):
234 """Checks whether a role has admin context.
235
236 If context_is_admin is contained in the policy file, then checks
237 whether the given role is contained in context_is_admin. If it is not
238 in the policy file, then default to context_is_admin: admin.
239 """
240 if 'context_is_admin' in self.rules.keys():
241 return self._allowed(
242 access=self._get_access_token(role),
243 apply_rule='context_is_admin')
Felipe Monteirof6b69e22017-05-04 21:55:04 +0100244 return role == CONF.identity.admin_role
DavidPurcell029d8c32017-01-06 15:27:41 -0500245
Felipe Monteirob0595652017-01-23 16:51:58 -0500246 def _get_access_token(self, role):
247 access_token = {
248 "token": {
249 "roles": [
250 {
251 "name": role
252 }
253 ],
Felipe Monteirofd1db982017-04-13 21:19:41 +0100254 "project_id": self.project_id,
255 "tenant_id": self.project_id,
Felipe Monteiro889264e2017-03-01 17:19:35 -0500256 "user_id": self.user_id
Felipe Monteirob0595652017-01-23 16:51:58 -0500257 }
258 }
259 return access_token
DavidPurcell029d8c32017-01-06 15:27:41 -0500260
Felipe Monteiro9c978502017-01-27 17:07:54 -0500261 def _allowed(self, access, apply_rule, is_admin=False):
Felipe Monteirof2b58d72017-08-31 22:40:36 +0100262 """Checks if a given rule in a policy is allowed with given ``access``.
DavidPurcell029d8c32017-01-06 15:27:41 -0500263
Felipe Monteirof2b58d72017-08-31 22:40:36 +0100264 :param dict access: Dictionary from ``_get_access_token``.
265 :param string apply_rule: Rule to be checked using ``oslo.policy``.
266 :param bool is_admin: Whether admin context is used.
DavidPurcell029d8c32017-01-06 15:27:41 -0500267 """
Felipe Monteirob0595652017-01-23 16:51:58 -0500268 access_data = copy.copy(access['token'])
269 access_data['roles'] = [role['name'] for role in access_data['roles']]
Felipe Monteirob0595652017-01-23 16:51:58 -0500270 access_data['is_admin'] = is_admin
Felipe Monteiro9c978502017-01-27 17:07:54 -0500271 # TODO(felipemonteiro): Dynamically calculate is_admin_project rather
272 # than hard-coding it to True. is_admin_project cannot be determined
273 # from the role, but rather from project and domain names. See
274 # _populate_is_admin_project in keystone.token.providers.common
275 # for more information.
276 access_data['is_admin_project'] = True
DavidPurcell029d8c32017-01-06 15:27:41 -0500277
Felipe Monteirob0595652017-01-23 16:51:58 -0500278 class Object(object):
279 pass
280 o = Object()
Felipe Monteiro9c978502017-01-27 17:07:54 -0500281 o.rules = self.rules
DavidPurcell029d8c32017-01-06 15:27:41 -0500282
Felipe Monteiro9fc782e2017-02-01 15:38:46 -0500283 target = {"project_id": access_data['project_id'],
284 "tenant_id": access_data['project_id'],
Felipe Monteiro889264e2017-03-01 17:19:35 -0500285 "network:tenant_id": access_data['project_id'],
286 "user_id": access_data['user_id']}
Felipe Monteirofd1db982017-04-13 21:19:41 +0100287 if self.extra_target_data:
288 target.update(self.extra_target_data)
Felipe Monteirob0595652017-01-23 16:51:58 -0500289
Felipe Monteiro9c978502017-01-27 17:07:54 -0500290 result = self._try_rule(apply_rule, target, access_data, o)
Felipe Monteirob0595652017-01-23 16:51:58 -0500291 return result
292
Felipe Monteiro9c978502017-01-27 17:07:54 -0500293 def _try_rule(self, apply_rule, target, access_data, o):
Samantha Blanco0d880082017-03-23 18:14:37 -0400294 if apply_rule not in self.rules:
Felipe Monteiro398a09f2018-01-23 01:39:25 +0000295 message = ("Policy action \"{0}\" not found in policy file: {1} or"
296 " among registered policy in code defaults for service."
297 ).format(apply_rule, self.path)
Felipe Monteiro48c913d2017-03-15 12:07:48 -0400298 LOG.debug(message)
299 raise rbac_exceptions.RbacParsingException(message)
Samantha Blanco0d880082017-03-23 18:14:37 -0400300 else:
301 rule = self.rules[apply_rule]
302 return rule(target, access_data, o)