blob: 27786aeee025f298b8f4ee8c76374a0acd928415 [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
Sergey Vilgelm062fb152018-09-06 20:51:57 -050016import collections
Felipe Monteirob0595652017-01-23 16:51:58 -050017import copy
Sergey Vilgelm062fb152018-09-06 20:51:57 -050018import glob
Felipe Monteiroae2ebab2017-03-23 22:49:06 +000019import json
DavidPurcell029d8c32017-01-06 15:27:41 -050020import os
21
DavidPurcell029d8c32017-01-06 15:27:41 -050022from oslo_log import log as logging
DavidPurcell029d8c32017-01-06 15:27:41 -050023from oslo_policy import policy
Felipe Monteiroae2ebab2017-03-23 22:49:06 +000024import stevedore
ghanshyam0df097d2017-08-08 09:28:17 +030025from tempest import clients
Rick Bartra503c5572017-03-09 13:49:58 -050026from tempest.common import credentials_factory as credentials
Felipe Monteiro7be94e82017-07-26 02:17:08 +010027from tempest import config
Rick Bartra503c5572017-03-09 13:49:58 -050028
Felipe Monteiro31e308e2018-05-22 12:05:10 -070029from patrole_tempest_plugin.rbac_authority import RbacAuthority
Felipe Monteirob0595652017-01-23 16:51:58 -050030from patrole_tempest_plugin import rbac_exceptions
DavidPurcell029d8c32017-01-06 15:27:41 -050031
Felipe Monteiro7be94e82017-07-26 02:17:08 +010032CONF = config.CONF
DavidPurcell029d8c32017-01-06 15:27:41 -050033LOG = logging.getLogger(__name__)
34
DavidPurcell029d8c32017-01-06 15:27:41 -050035
Felipe Monteiro88a5bab2017-08-31 04:00:32 +010036class PolicyAuthority(RbacAuthority):
Felipe Monteirof2b58d72017-08-31 22:40:36 +010037 """A class that uses ``oslo.policy`` for validating RBAC."""
DavidPurcell029d8c32017-01-06 15:27:41 -050038
Felipe Monteiro0854ded2017-05-05 16:30:55 +010039 def __init__(self, project_id, user_id, service, extra_target_data=None):
Felipe Monteirof2b58d72017-08-31 22:40:36 +010040 """Initialization of Policy Authority class.
DavidPurcell029d8c32017-01-06 15:27:41 -050041
Felipe Monteirof2b58d72017-08-31 22:40:36 +010042 Validates whether a test role can perform a policy action by querying
43 ``oslo.policy`` with necessary test data.
Felipe Monteiro9c978502017-01-27 17:07:54 -050044
Felipe Monteirof2b58d72017-08-31 22:40:36 +010045 If a policy file does not exist, checks whether the policy file is
46 registered as a namespace under "oslo.policy.policies". Nova, for
47 example, doesn't use a policy file by default; its policies are
48 implemented in code and registered as "nova" under
49 "oslo.policy.policies".
Felipe Monteiro9c978502017-01-27 17:07:54 -050050
Felipe Monteirof2b58d72017-08-31 22:40:36 +010051 If the policy file is not found in either code or in a policy file,
52 then an exception is raised.
53
54 Additionally, if a custom policy file exists along with the default
55 policy in code implementation, the custom policy is prioritized.
Felipe Monteirob0595652017-01-23 16:51:58 -050056
Felipe Monteiro3ab2c352017-07-05 22:25:34 +010057 :param uuid project_id: project_id of object performing API call
58 :param uuid user_id: user_id of object performing API call
59 :param string service: service of the policy file
60 :param dict extra_target_data: dictionary containing additional object
61 data needed by oslo.policy to validate generic checks
Felipe Monteirof2b58d72017-08-31 22:40:36 +010062
63 Example:
64
65 .. code-block:: python
66
67 # Below is the default policy implementation in code, defined in
68 # a service like Nova.
69 test_policies = [
70 policy.DocumentedRuleDefault(
71 'service:test_rule',
72 base.RULE_ADMIN_OR_OWNER,
73 "This is a description for a test policy",
74 [
75 {
76 'method': 'POST',
77 'path': '/path/to/test/resource'
78 }
79 ]),
80 'service:another_test_rule',
81 base.RULE_ADMIN_OR_OWNER,
82 "This is a description for another test policy",
83 [
84 {
85 'method': 'GET',
86 'path': '/path/to/test/resource'
87 }
88 ]),
89 ]
90
91 .. code-block:: yaml
92
93 # Below is the custom override of the default policy in a YAML
94 # policy file. Note that the default rule is "rule:admin_or_owner"
95 # and the custom rule is "rule:admin_api". The `PolicyAuthority`
96 # class will use the "rule:admin_api" definition for this policy
97 # action.
98 "service:test_rule" : "rule:admin_api"
99
100 # Note below that no override is provided for
101 # "service:another_test_rule", which means that the default policy
102 # rule is used: "rule:admin_or_owner".
DavidPurcell029d8c32017-01-06 15:27:41 -0500103 """
Felipe Monteiroae2ebab2017-03-23 22:49:06 +0000104
Felipe Monteiro0854ded2017-05-05 16:30:55 +0100105 if extra_target_data is None:
106 extra_target_data = {}
107
Sergey Vilgelm062fb152018-09-06 20:51:57 -0500108 self.service = self.validate_service(service)
Rick Bartra503c5572017-03-09 13:49:58 -0500109
Felipe Monteiro3ab2c352017-07-05 22:25:34 +0100110 # Prioritize dynamically searching for policy files over relying on
111 # deprecated service-specific policy file locations.
Felipe Monteirof6eb8622017-08-06 06:08:02 +0100112 if CONF.patrole.custom_policy_files:
Felipe Monteiro3ab2c352017-07-05 22:25:34 +0100113 self.discover_policy_files()
Felipe Monteiro3ab2c352017-07-05 22:25:34 +0100114
Sergey Vilgelm062fb152018-09-06 20:51:57 -0500115 self.rules = policy.Rules.load(self._get_policy_data(), 'default')
Felipe Monteirofd1db982017-04-13 21:19:41 +0100116 self.project_id = project_id
Felipe Monteiro889264e2017-03-01 17:19:35 -0500117 self.user_id = user_id
Felipe Monteirofd1db982017-04-13 21:19:41 +0100118 self.extra_target_data = extra_target_data
DavidPurcell029d8c32017-01-06 15:27:41 -0500119
Felipe Monteirod9607c42017-06-12 19:28:45 +0100120 @classmethod
121 def validate_service(cls, service):
Felipe Monteiro3ab2c352017-07-05 22:25:34 +0100122 """Validate whether the service passed to ``__init__`` exists."""
Felipe Monteirod9607c42017-06-12 19:28:45 +0100123 service = service.lower().strip() if service else None
124
125 # Cache the list of available services in memory to avoid needlessly
126 # doing an API call every time.
127 if not hasattr(cls, 'available_services'):
ghanshyam0df097d2017-08-08 09:28:17 +0300128 admin_mgr = clients.Manager(
129 credentials.get_configured_admin_credentials())
Felipe Monteiro7be94e82017-07-26 02:17:08 +0100130 services_client = (admin_mgr.identity_services_v3_client
131 if CONF.identity_feature_enabled.api_v3
132 else admin_mgr.identity_services_client)
133 services = services_client.list_services()['services']
Felipe Monteirod9607c42017-06-12 19:28:45 +0100134 cls.available_services = [s['name'] for s in services]
135
136 if not service or service not in cls.available_services:
137 LOG.debug("%s is NOT a valid service.", service)
Felipe Monteiro51299a12018-06-28 20:03:27 -0400138 raise rbac_exceptions.RbacInvalidServiceException(
Felipe Monteirod9607c42017-06-12 19:28:45 +0100139 "%s is NOT a valid service." % service)
140
Sergey Vilgelm062fb152018-09-06 20:51:57 -0500141 return service
142
Felipe Monteiro3ab2c352017-07-05 22:25:34 +0100143 @classmethod
144 def discover_policy_files(cls):
Felipe Monteirof2b58d72017-08-31 22:40:36 +0100145 """Dynamically discover the policy file for each service in
Sergey Vilgelm062fb152018-09-06 20:51:57 -0500146 ``cls.available_services``. Pick all candidate paths found
Felipe Monteirof2b58d72017-08-31 22:40:36 +0100147 out of the potential paths in ``[patrole] custom_policy_files``.
148 """
Felipe Monteiro3ab2c352017-07-05 22:25:34 +0100149 if not hasattr(cls, 'policy_files'):
Sergey Vilgelm062fb152018-09-06 20:51:57 -0500150 cls.policy_files = collections.defaultdict(list)
Felipe Monteiro3ab2c352017-07-05 22:25:34 +0100151 for service in cls.available_services:
Felipe Monteirof6eb8622017-08-06 06:08:02 +0100152 for candidate_path in CONF.patrole.custom_policy_files:
Sergey Vilgelm062fb152018-09-06 20:51:57 -0500153 path = candidate_path % service
154 for filename in glob.iglob(path):
155 if os.path.isfile(filename):
156 cls.policy_files[service].append(filename)
Felipe Monteiro3ab2c352017-07-05 22:25:34 +0100157
Felipe Monteirob0595652017-01-23 16:51:58 -0500158 def allowed(self, rule_name, role):
Felipe Monteirof2b58d72017-08-31 22:40:36 +0100159 """Checks if a given rule in a policy is allowed with given role.
160
Felipe Monteiro778b7802018-05-31 19:52:58 -0400161 :param string rule_name: Policy name to pass to``oslo.policy``.
162 :param string role: Role to validate for authorization.
163 :raises RbacParsingException: If ``rule_name`` does not exist in the
Felipe Monteiro4ef7e532018-03-11 07:17:11 -0400164 cloud (in policy file or among registered in-code policy defaults).
Felipe Monteirof2b58d72017-08-31 22:40:36 +0100165 """
Felipe Monteiro9c978502017-01-27 17:07:54 -0500166 is_admin_context = self._is_admin_context(role)
Felipe Monteirob0595652017-01-23 16:51:58 -0500167 is_allowed = self._allowed(
Felipe Monteiro9c978502017-01-27 17:07:54 -0500168 access=self._get_access_token(role),
Felipe Monteirob0595652017-01-23 16:51:58 -0500169 apply_rule=rule_name,
Felipe Monteiro9c978502017-01-27 17:07:54 -0500170 is_admin=is_admin_context)
Felipe Monteiro9c978502017-01-27 17:07:54 -0500171 return is_allowed
DavidPurcell029d8c32017-01-06 15:27:41 -0500172
Sergey Vilgelm062fb152018-09-06 20:51:57 -0500173 def _get_policy_data(self):
Felipe Monteiroae2ebab2017-03-23 22:49:06 +0000174 file_policy_data = {}
175 mgr_policy_data = {}
176 policy_data = {}
177
Felipe Monteiro3ab2c352017-07-05 22:25:34 +0100178 # Check whether policy file exists and attempt to read it.
Sergey Vilgelm062fb152018-09-06 20:51:57 -0500179 for path in self.policy_files[self.service]:
Felipe Monteiroae2ebab2017-03-23 22:49:06 +0000180 try:
Sergey Vilgelm062fb152018-09-06 20:51:57 -0500181 with open(path, 'r') as fp:
182 for k, v in json.load(fp).items():
183 if k not in file_policy_data:
184 file_policy_data[k] = v
185 else:
186 # If the policy name and rule are the same, no
187 # ambiguity, so no reason to warn.
188 if v != file_policy_data[k]:
189 LOG.warning(
190 "The same policy name: %s was found in "
191 "multiple policies files for service %s. "
192 "This can lead to policy rule ambiguity. "
193 "Using rule: %s", k, self.service,
194 file_policy_data[k])
Samantha Blanco85f79d72017-04-21 11:09:14 -0400195 except (IOError, ValueError) as e:
196 msg = "Failed to read policy file for service. "
197 if isinstance(e, IOError):
198 msg += "Please check that policy path exists."
199 else:
200 msg += "JSON may be improperly formatted."
201 LOG.debug(msg)
Felipe Monteiroae2ebab2017-03-23 22:49:06 +0000202
203 # Check whether policy actions are defined in code. Nova and Keystone,
204 # for example, define their default policy actions in code.
205 mgr = stevedore.named.NamedExtensionManager(
206 'oslo.policy.policies',
Sergey Vilgelm062fb152018-09-06 20:51:57 -0500207 names=[self.service],
Felipe Monteiroae2ebab2017-03-23 22:49:06 +0000208 on_load_failure_callback=None,
209 invoke_on_load=True,
210 warn_on_missing_entrypoint=False)
211
212 if mgr:
Sergey Vilgelm062fb152018-09-06 20:51:57 -0500213 policy_generator = {plc.name: plc.obj for plc in mgr}
214 if policy_generator and self.service in policy_generator:
215 for rule in policy_generator[self.service]:
Felipe Monteiroae2ebab2017-03-23 22:49:06 +0000216 mgr_policy_data[rule.name] = str(rule.check)
217
218 # If data from both file and code exist, combine both together.
219 if file_policy_data and mgr_policy_data:
220 # Add the policy actions from code first.
221 for action, rule in mgr_policy_data.items():
222 policy_data[action] = rule
223 # Overwrite with any custom policy actions defined in policy.json.
224 for action, rule in file_policy_data.items():
225 policy_data[action] = rule
226 elif file_policy_data:
227 policy_data = file_policy_data
228 elif mgr_policy_data:
229 policy_data = mgr_policy_data
230 else:
Felipe Monteiro3ab2c352017-07-05 22:25:34 +0100231 error_message = (
Sergey Vilgelm062fb152018-09-06 20:51:57 -0500232 'Policy files for {0} service were not found among the '
Felipe Monteiro4ef7e532018-03-11 07:17:11 -0400233 'registered in-code policies or in any of the possible policy '
Sergey Vilgelm062fb152018-09-06 20:51:57 -0500234 'files: {1}.'.format(self.service,
235 [loc % self.service for loc in
Felipe Monteiro4ef7e532018-03-11 07:17:11 -0400236 CONF.patrole.custom_policy_files])
Felipe Monteiro3ab2c352017-07-05 22:25:34 +0100237 )
Felipe Monteiroae2ebab2017-03-23 22:49:06 +0000238 raise rbac_exceptions.RbacParsingException(error_message)
239
240 try:
241 policy_data = json.dumps(policy_data)
Felipe Monteirob5809632017-10-26 04:12:12 +0100242 except (TypeError, ValueError):
Sergey Vilgelm062fb152018-09-06 20:51:57 -0500243 error_message = 'Policy files for {0} service are invalid.'.format(
244 self.service)
Felipe Monteiroae2ebab2017-03-23 22:49:06 +0000245 raise rbac_exceptions.RbacParsingException(error_message)
246
247 return policy_data
248
Felipe Monteiro9c978502017-01-27 17:07:54 -0500249 def _is_admin_context(self, role):
250 """Checks whether a role has admin context.
251
252 If context_is_admin is contained in the policy file, then checks
253 whether the given role is contained in context_is_admin. If it is not
254 in the policy file, then default to context_is_admin: admin.
255 """
256 if 'context_is_admin' in self.rules.keys():
257 return self._allowed(
258 access=self._get_access_token(role),
259 apply_rule='context_is_admin')
Felipe Monteirof6b69e22017-05-04 21:55:04 +0100260 return role == CONF.identity.admin_role
DavidPurcell029d8c32017-01-06 15:27:41 -0500261
Felipe Monteirob0595652017-01-23 16:51:58 -0500262 def _get_access_token(self, role):
263 access_token = {
264 "token": {
265 "roles": [
266 {
267 "name": role
268 }
269 ],
Felipe Monteirofd1db982017-04-13 21:19:41 +0100270 "project_id": self.project_id,
271 "tenant_id": self.project_id,
Felipe Monteiro889264e2017-03-01 17:19:35 -0500272 "user_id": self.user_id
Felipe Monteirob0595652017-01-23 16:51:58 -0500273 }
274 }
275 return access_token
DavidPurcell029d8c32017-01-06 15:27:41 -0500276
Felipe Monteiro9c978502017-01-27 17:07:54 -0500277 def _allowed(self, access, apply_rule, is_admin=False):
Felipe Monteirof2b58d72017-08-31 22:40:36 +0100278 """Checks if a given rule in a policy is allowed with given ``access``.
DavidPurcell029d8c32017-01-06 15:27:41 -0500279
Felipe Monteirof2b58d72017-08-31 22:40:36 +0100280 :param dict access: Dictionary from ``_get_access_token``.
281 :param string apply_rule: Rule to be checked using ``oslo.policy``.
282 :param bool is_admin: Whether admin context is used.
DavidPurcell029d8c32017-01-06 15:27:41 -0500283 """
Felipe Monteirob0595652017-01-23 16:51:58 -0500284 access_data = copy.copy(access['token'])
285 access_data['roles'] = [role['name'] for role in access_data['roles']]
Felipe Monteirob0595652017-01-23 16:51:58 -0500286 access_data['is_admin'] = is_admin
Felipe Monteiro9c978502017-01-27 17:07:54 -0500287 # TODO(felipemonteiro): Dynamically calculate is_admin_project rather
288 # than hard-coding it to True. is_admin_project cannot be determined
Felipe Monteiro94fc2ca2018-05-22 12:08:43 -0700289 # from the role, but rather from project and domain names. For more
290 # information, see:
291 # https://github.com/openstack/keystone/blob/37ce5417418f8acbd27f3dacb70c605b0fe48301/keystone/token/providers/common.py#L150
Felipe Monteiro9c978502017-01-27 17:07:54 -0500292 access_data['is_admin_project'] = True
DavidPurcell029d8c32017-01-06 15:27:41 -0500293
Felipe Monteirob0595652017-01-23 16:51:58 -0500294 class Object(object):
295 pass
296 o = Object()
Felipe Monteiro9c978502017-01-27 17:07:54 -0500297 o.rules = self.rules
DavidPurcell029d8c32017-01-06 15:27:41 -0500298
Felipe Monteiro9fc782e2017-02-01 15:38:46 -0500299 target = {"project_id": access_data['project_id'],
300 "tenant_id": access_data['project_id'],
Felipe Monteiro889264e2017-03-01 17:19:35 -0500301 "network:tenant_id": access_data['project_id'],
302 "user_id": access_data['user_id']}
Felipe Monteirofd1db982017-04-13 21:19:41 +0100303 if self.extra_target_data:
304 target.update(self.extra_target_data)
Felipe Monteirob0595652017-01-23 16:51:58 -0500305
Felipe Monteiro9c978502017-01-27 17:07:54 -0500306 result = self._try_rule(apply_rule, target, access_data, o)
Felipe Monteirob0595652017-01-23 16:51:58 -0500307 return result
308
Felipe Monteiro9c978502017-01-27 17:07:54 -0500309 def _try_rule(self, apply_rule, target, access_data, o):
Samantha Blanco0d880082017-03-23 18:14:37 -0400310 if apply_rule not in self.rules:
Sergey Vilgelm062fb152018-09-06 20:51:57 -0500311 message = ('Policy action "{0}" not found in policy files: '
312 '{1} or among registered policy in code defaults for '
313 '{2} service.').format(apply_rule,
314 self.policy_files[self.service],
315 self.service)
Felipe Monteiro48c913d2017-03-15 12:07:48 -0400316 LOG.debug(message)
317 raise rbac_exceptions.RbacParsingException(message)
Samantha Blanco0d880082017-03-23 18:14:37 -0400318 else:
319 rule = self.rules[apply_rule]
320 return rule(target, access_data, o)