blob: 914f2f9ad8da1064c364b2ce0f83aeb48549d2e1 [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
DavidPurcell029d8c32017-01-06 15:27:41 -050019import os
20
DavidPurcell029d8c32017-01-06 15:27:41 -050021from oslo_log import log as logging
DavidPurcell029d8c32017-01-06 15:27:41 -050022from oslo_policy import policy
Ghanshyam Mannf64b81e2021-03-15 11:52:41 -050023import pkg_resources
Felipe Monteiroae2ebab2017-03-23 22:49:06 +000024import stevedore
Felipe Monteiro7be94e82017-07-26 02:17:08 +010025from tempest import config
Rick Bartra503c5572017-03-09 13:49:58 -050026
Felipe Monteiro31e308e2018-05-22 12:05:10 -070027from patrole_tempest_plugin.rbac_authority import RbacAuthority
Felipe Monteirob0595652017-01-23 16:51:58 -050028from patrole_tempest_plugin import rbac_exceptions
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."""
Lingxian Kong27f671f2020-12-30 21:23:03 +130036 os_admin = None
DavidPurcell029d8c32017-01-06 15:27:41 -050037
Felipe Monteiro0854ded2017-05-05 16:30:55 +010038 def __init__(self, project_id, user_id, service, extra_target_data=None):
Felipe Monteirof2b58d72017-08-31 22:40:36 +010039 """Initialization of Policy Authority class.
DavidPurcell029d8c32017-01-06 15:27:41 -050040
Felipe Monteirof2b58d72017-08-31 22:40:36 +010041 Validates whether a test role can perform a policy action by querying
42 ``oslo.policy`` with necessary test data.
Felipe Monteiro9c978502017-01-27 17:07:54 -050043
Felipe Monteirof2b58d72017-08-31 22:40:36 +010044 If a policy file does not exist, checks whether the policy file is
45 registered as a namespace under "oslo.policy.policies". Nova, for
46 example, doesn't use a policy file by default; its policies are
47 implemented in code and registered as "nova" under
48 "oslo.policy.policies".
Felipe Monteiro9c978502017-01-27 17:07:54 -050049
Felipe Monteirof2b58d72017-08-31 22:40:36 +010050 If the policy file is not found in either code or in a policy file,
51 then an exception is raised.
52
53 Additionally, if a custom policy file exists along with the default
54 policy in code implementation, the custom policy is prioritized.
Felipe Monteirob0595652017-01-23 16:51:58 -050055
Felipe Monteiro3ab2c352017-07-05 22:25:34 +010056 :param uuid project_id: project_id of object performing API call
57 :param uuid user_id: user_id of object performing API call
58 :param string service: service of the policy file
59 :param dict extra_target_data: dictionary containing additional object
60 data needed by oslo.policy to validate generic checks
Felipe Monteirof2b58d72017-08-31 22:40:36 +010061
62 Example:
63
64 .. code-block:: python
65
66 # Below is the default policy implementation in code, defined in
67 # a service like Nova.
68 test_policies = [
69 policy.DocumentedRuleDefault(
70 'service:test_rule',
71 base.RULE_ADMIN_OR_OWNER,
72 "This is a description for a test policy",
73 [
74 {
75 'method': 'POST',
76 'path': '/path/to/test/resource'
77 }
78 ]),
79 'service:another_test_rule',
80 base.RULE_ADMIN_OR_OWNER,
81 "This is a description for another test policy",
82 [
83 {
84 'method': 'GET',
85 'path': '/path/to/test/resource'
86 }
87 ]),
88 ]
89
90 .. code-block:: yaml
91
92 # Below is the custom override of the default policy in a YAML
93 # policy file. Note that the default rule is "rule:admin_or_owner"
94 # and the custom rule is "rule:admin_api". The `PolicyAuthority`
95 # class will use the "rule:admin_api" definition for this policy
96 # action.
97 "service:test_rule" : "rule:admin_api"
98
99 # Note below that no override is provided for
100 # "service:another_test_rule", which means that the default policy
101 # rule is used: "rule:admin_or_owner".
DavidPurcell029d8c32017-01-06 15:27:41 -0500102 """
Felipe Monteiroae2ebab2017-03-23 22:49:06 +0000103
Felipe Monteiro0854ded2017-05-05 16:30:55 +0100104 if extra_target_data is None:
105 extra_target_data = {}
106
Sergey Vilgelm062fb152018-09-06 20:51:57 -0500107 self.service = self.validate_service(service)
Rick Bartra503c5572017-03-09 13:49:58 -0500108
Felipe Monteiro3ab2c352017-07-05 22:25:34 +0100109 # Prioritize dynamically searching for policy files over relying on
110 # deprecated service-specific policy file locations.
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()
Felipe Monteiro3ab2c352017-07-05 22:25:34 +0100113
Sergey Vilgelmef7047d2018-09-11 14:48:55 -0500114 self.rules = self.get_rules()
Felipe Monteirofd1db982017-04-13 21:19:41 +0100115 self.project_id = project_id
Felipe Monteiro889264e2017-03-01 17:19:35 -0500116 self.user_id = user_id
Felipe Monteirofd1db982017-04-13 21:19:41 +0100117 self.extra_target_data = extra_target_data
DavidPurcell029d8c32017-01-06 15:27:41 -0500118
Felipe Monteirod9607c42017-06-12 19:28:45 +0100119 @classmethod
120 def validate_service(cls, service):
Felipe Monteiro3ab2c352017-07-05 22:25:34 +0100121 """Validate whether the service passed to ``__init__`` exists."""
Felipe Monteirod9607c42017-06-12 19:28:45 +0100122 service = service.lower().strip() if service else None
123
124 # Cache the list of available services in memory to avoid needlessly
125 # doing an API call every time.
Lingxian Kong27f671f2020-12-30 21:23:03 +1300126 if not hasattr(cls, 'available_services') and cls.os_admin:
127 services_client = (cls.os_admin.identity_services_v3_client
Felipe Monteiro7be94e82017-07-26 02:17:08 +0100128 if CONF.identity_feature_enabled.api_v3
Lingxian Kong27f671f2020-12-30 21:23:03 +1300129 else cls.os_admin.identity_services_client)
Felipe Monteiro7be94e82017-07-26 02:17:08 +0100130 services = services_client.list_services()['services']
Felipe Monteirod9607c42017-06-12 19:28:45 +0100131 cls.available_services = [s['name'] for s in services]
132
133 if not service or service not in cls.available_services:
134 LOG.debug("%s is NOT a valid service.", service)
Felipe Monteiro51299a12018-06-28 20:03:27 -0400135 raise rbac_exceptions.RbacInvalidServiceException(
Felipe Monteirod9607c42017-06-12 19:28:45 +0100136 "%s is NOT a valid service." % service)
137
Sergey Vilgelm062fb152018-09-06 20:51:57 -0500138 return service
139
Felipe Monteiro3ab2c352017-07-05 22:25:34 +0100140 @classmethod
141 def discover_policy_files(cls):
Felipe Monteirof2b58d72017-08-31 22:40:36 +0100142 """Dynamically discover the policy file for each service in
Sergey Vilgelm062fb152018-09-06 20:51:57 -0500143 ``cls.available_services``. Pick all candidate paths found
Felipe Monteirof2b58d72017-08-31 22:40:36 +0100144 out of the potential paths in ``[patrole] custom_policy_files``.
145 """
Felipe Monteiro3ab2c352017-07-05 22:25:34 +0100146 if not hasattr(cls, 'policy_files'):
Sergey Vilgelm062fb152018-09-06 20:51:57 -0500147 cls.policy_files = collections.defaultdict(list)
Felipe Monteiro3ab2c352017-07-05 22:25:34 +0100148 for service in cls.available_services:
Felipe Monteirof6eb8622017-08-06 06:08:02 +0100149 for candidate_path in CONF.patrole.custom_policy_files:
Sergey Vilgelm062fb152018-09-06 20:51:57 -0500150 path = candidate_path % service
151 for filename in glob.iglob(path):
152 if os.path.isfile(filename):
153 cls.policy_files[service].append(filename)
Felipe Monteiro3ab2c352017-07-05 22:25:34 +0100154
Mykola Yakovlieve0f35502018-09-26 18:26:57 -0500155 def allowed(self, rule_name, roles):
Felipe Monteirof2b58d72017-08-31 22:40:36 +0100156 """Checks if a given rule in a policy is allowed with given role.
157
Felipe Monteiro778b7802018-05-31 19:52:58 -0400158 :param string rule_name: Policy name to pass to``oslo.policy``.
Mykola Yakovlieve0f35502018-09-26 18:26:57 -0500159 :param List[string] roles: List of roles to validate for authorization.
Felipe Monteiro778b7802018-05-31 19:52:58 -0400160 :raises RbacParsingException: If ``rule_name`` does not exist in the
Felipe Monteiro4ef7e532018-03-11 07:17:11 -0400161 cloud (in policy file or among registered in-code policy defaults).
Felipe Monteirof2b58d72017-08-31 22:40:36 +0100162 """
Mykola Yakovlieve0f35502018-09-26 18:26:57 -0500163 is_admin_context = self._is_admin_context(roles)
Felipe Monteirob0595652017-01-23 16:51:58 -0500164 is_allowed = self._allowed(
Mykola Yakovlieve0f35502018-09-26 18:26:57 -0500165 access=self._get_access_token(roles),
Felipe Monteirob0595652017-01-23 16:51:58 -0500166 apply_rule=rule_name,
Felipe Monteiro9c978502017-01-27 17:07:54 -0500167 is_admin=is_admin_context)
Felipe Monteiro9c978502017-01-27 17:07:54 -0500168 return is_allowed
DavidPurcell029d8c32017-01-06 15:27:41 -0500169
Sergey Vilgelm55e5dfe2019-01-07 11:59:41 -0600170 def _handle_deprecated_rule(self, default):
171 deprecated_rule = default.deprecated_rule
172 deprecated_msg = (
173 'Policy "%(old_name)s":"%(old_check_str)s" was deprecated in '
174 '%(release)s in favor of "%(name)s":"%(check_str)s". Reason: '
175 '%(reason)s. Either ensure your deployment is ready for the new '
176 'default or copy/paste the deprecated policy into your policy '
177 'file and maintain it manually.' % {
178 'old_name': deprecated_rule.name,
179 'old_check_str': deprecated_rule.check_str,
180 'release': default.deprecated_since,
181 'name': default.name,
182 'check_str': default.check_str,
183 'reason': default.deprecated_reason
184 }
185 )
186 LOG.warn(deprecated_msg)
Ghanshyam Mannf64b81e2021-03-15 11:52:41 -0500187 oslo_policy_version = pkg_resources.parse_version(
188 pkg_resources.get_distribution("oslo.policy").version)
189 # NOTE(gmann): oslo policy 3.7.0 onwards does not allow to modify
190 # the Rule object check attribute.
191 required_version = pkg_resources.parse_version('3.7.0')
192 if oslo_policy_version >= required_version:
193 return policy.OrCheck([default.check, deprecated_rule.check])
194 else:
195 default.check = policy.OrCheck(
196 [policy._parser.parse_rule(cs) for cs in
197 [default.check_str,
198 deprecated_rule.check_str]])
199 return default.check
Sergey Vilgelm55e5dfe2019-01-07 11:59:41 -0600200
Sergey Vilgelmef7047d2018-09-11 14:48:55 -0500201 def get_rules(self):
202 rules = policy.Rules()
Felipe Monteiro3ab2c352017-07-05 22:25:34 +0100203 # Check whether policy file exists and attempt to read it.
Sergey Vilgelm062fb152018-09-06 20:51:57 -0500204 for path in self.policy_files[self.service]:
Felipe Monteiroae2ebab2017-03-23 22:49:06 +0000205 try:
Sergey Vilgelm062fb152018-09-06 20:51:57 -0500206 with open(path, 'r') as fp:
Sergey Vilgelmef7047d2018-09-11 14:48:55 -0500207 for k, v in policy.Rules.load(fp.read()).items():
208 if k not in rules:
209 rules[k] = v
210 # If the policy name and rule are the same, no
211 # ambiguity, so no reason to warn.
212 elif str(v) != str(rules[k]):
213 msg = ("The same policy name: %s was found in "
214 "multiple policies files for service %s. "
215 "This can lead to policy rule ambiguity. "
216 "Using rule: %s; Rule from file: %s")
217 LOG.warning(msg, k, self.service, rules[k], v)
218 except (ValueError, IOError):
219 LOG.warning("Failed to read policy file '%s' for service %s.",
220 path, self.service)
Felipe Monteiroae2ebab2017-03-23 22:49:06 +0000221
222 # Check whether policy actions are defined in code. Nova and Keystone,
223 # for example, define their default policy actions in code.
224 mgr = stevedore.named.NamedExtensionManager(
225 'oslo.policy.policies',
Sergey Vilgelm062fb152018-09-06 20:51:57 -0500226 names=[self.service],
Felipe Monteiroae2ebab2017-03-23 22:49:06 +0000227 invoke_on_load=True,
228 warn_on_missing_entrypoint=False)
229
230 if mgr:
Sergey Vilgelm062fb152018-09-06 20:51:57 -0500231 policy_generator = {plc.name: plc.obj for plc in mgr}
Sergey Vilgelmef7047d2018-09-11 14:48:55 -0500232 if self.service in policy_generator:
Sergey Vilgelm062fb152018-09-06 20:51:57 -0500233 for rule in policy_generator[self.service]:
Sergey Vilgelmef7047d2018-09-11 14:48:55 -0500234 if rule.name not in rules:
Sergey Vilgelm55e5dfe2019-01-07 11:59:41 -0600235 if CONF.patrole.validate_deprecated_rules:
236 # NOTE (sergey.vilgelm):
237 # The `DocumentedRuleDefault` object has no
238 # `deprecated_rule` attribute in Pike
Ghanshyam Mannf64b81e2021-03-15 11:52:41 -0500239 check = rule.check
Sergey Vilgelm55e5dfe2019-01-07 11:59:41 -0600240 if getattr(rule, 'deprecated_rule', False):
Ghanshyam Mannf64b81e2021-03-15 11:52:41 -0500241 check = self._handle_deprecated_rule(rule)
242 rules[rule.name] = check
Sergey Vilgelmef7047d2018-09-11 14:48:55 -0500243 elif str(rule.check) != str(rules[rule.name]):
244 msg = ("The same policy name: %s was found in the "
245 "policies files and in the code for service "
246 "%s. This can lead to policy rule ambiguity. "
247 "Using rule: %s; Rule from code: %s")
248 LOG.warning(msg, rule.name, self.service,
249 rules[rule.name], rule.check)
Felipe Monteiroae2ebab2017-03-23 22:49:06 +0000250
Sergey Vilgelmef7047d2018-09-11 14:48:55 -0500251 if not rules:
252 msg = (
Sergey Vilgelm062fb152018-09-06 20:51:57 -0500253 'Policy files for {0} service were not found among the '
Felipe Monteiro4ef7e532018-03-11 07:17:11 -0400254 'registered in-code policies or in any of the possible policy '
Sergey Vilgelmef7047d2018-09-11 14:48:55 -0500255 'files: {1}.'.format(
256 self.service,
257 [loc % self.service
258 for loc in CONF.patrole.custom_policy_files]))
259 raise rbac_exceptions.RbacParsingException(msg)
Felipe Monteiroae2ebab2017-03-23 22:49:06 +0000260
Sergey Vilgelmef7047d2018-09-11 14:48:55 -0500261 return rules
Felipe Monteiroae2ebab2017-03-23 22:49:06 +0000262
Mykola Yakovlieve0f35502018-09-26 18:26:57 -0500263 def _is_admin_context(self, roles):
Felipe Monteiro9c978502017-01-27 17:07:54 -0500264 """Checks whether a role has admin context.
265
266 If context_is_admin is contained in the policy file, then checks
267 whether the given role is contained in context_is_admin. If it is not
268 in the policy file, then default to context_is_admin: admin.
269 """
Manik Bindlish0868ded2018-12-20 08:44:50 +0000270 if 'context_is_admin' in self.rules:
Felipe Monteiro9c978502017-01-27 17:07:54 -0500271 return self._allowed(
Mykola Yakovlieve0f35502018-09-26 18:26:57 -0500272 access=self._get_access_token(roles),
Felipe Monteiro9c978502017-01-27 17:07:54 -0500273 apply_rule='context_is_admin')
Mykola Yakovlieve0f35502018-09-26 18:26:57 -0500274 return CONF.identity.admin_role in roles
DavidPurcell029d8c32017-01-06 15:27:41 -0500275
Mykola Yakovlieve0f35502018-09-26 18:26:57 -0500276 def _get_access_token(self, roles):
Felipe Monteirob0595652017-01-23 16:51:58 -0500277 access_token = {
278 "token": {
Sergey Vilgelm55e5dfe2019-01-07 11:59:41 -0600279 "roles": [{'name': r} for r in roles],
Felipe Monteirofd1db982017-04-13 21:19:41 +0100280 "project_id": self.project_id,
281 "tenant_id": self.project_id,
Felipe Monteiro889264e2017-03-01 17:19:35 -0500282 "user_id": self.user_id
Felipe Monteirob0595652017-01-23 16:51:58 -0500283 }
284 }
285 return access_token
DavidPurcell029d8c32017-01-06 15:27:41 -0500286
Felipe Monteiro9c978502017-01-27 17:07:54 -0500287 def _allowed(self, access, apply_rule, is_admin=False):
Felipe Monteirof2b58d72017-08-31 22:40:36 +0100288 """Checks if a given rule in a policy is allowed with given ``access``.
DavidPurcell029d8c32017-01-06 15:27:41 -0500289
Felipe Monteirof2b58d72017-08-31 22:40:36 +0100290 :param dict access: Dictionary from ``_get_access_token``.
291 :param string apply_rule: Rule to be checked using ``oslo.policy``.
292 :param bool is_admin: Whether admin context is used.
DavidPurcell029d8c32017-01-06 15:27:41 -0500293 """
Felipe Monteirob0595652017-01-23 16:51:58 -0500294 access_data = copy.copy(access['token'])
295 access_data['roles'] = [role['name'] for role in access_data['roles']]
Felipe Monteirob0595652017-01-23 16:51:58 -0500296 access_data['is_admin'] = is_admin
Felipe Monteiro9c978502017-01-27 17:07:54 -0500297 # TODO(felipemonteiro): Dynamically calculate is_admin_project rather
298 # than hard-coding it to True. is_admin_project cannot be determined
Felipe Monteiro94fc2ca2018-05-22 12:08:43 -0700299 # from the role, but rather from project and domain names. For more
300 # information, see:
Luigi Toscano6da06ed2019-01-07 17:50:41 +0100301 # https://git.openstack.org/cgit/openstack/keystone/tree/keystone/token/providers/common.py?id=37ce5417418f8acbd27f3dacb70c605b0fe48301#n150
Felipe Monteiro9c978502017-01-27 17:07:54 -0500302 access_data['is_admin_project'] = True
DavidPurcell029d8c32017-01-06 15:27:41 -0500303
Felipe Monteirob0595652017-01-23 16:51:58 -0500304 class Object(object):
305 pass
306 o = Object()
Felipe Monteiro9c978502017-01-27 17:07:54 -0500307 o.rules = self.rules
DavidPurcell029d8c32017-01-06 15:27:41 -0500308
Felipe Monteiro9fc782e2017-02-01 15:38:46 -0500309 target = {"project_id": access_data['project_id'],
310 "tenant_id": access_data['project_id'],
Felipe Monteiro889264e2017-03-01 17:19:35 -0500311 "network:tenant_id": access_data['project_id'],
312 "user_id": access_data['user_id']}
Felipe Monteirofd1db982017-04-13 21:19:41 +0100313 if self.extra_target_data:
314 target.update(self.extra_target_data)
Felipe Monteirob0595652017-01-23 16:51:58 -0500315
Felipe Monteiro9c978502017-01-27 17:07:54 -0500316 result = self._try_rule(apply_rule, target, access_data, o)
Felipe Monteirob0595652017-01-23 16:51:58 -0500317 return result
318
Felipe Monteiro9c978502017-01-27 17:07:54 -0500319 def _try_rule(self, apply_rule, target, access_data, o):
Samantha Blanco0d880082017-03-23 18:14:37 -0400320 if apply_rule not in self.rules:
Sergey Vilgelm062fb152018-09-06 20:51:57 -0500321 message = ('Policy action "{0}" not found in policy files: '
322 '{1} or among registered policy in code defaults for '
323 '{2} service.').format(apply_rule,
324 self.policy_files[self.service],
325 self.service)
Felipe Monteiro48c913d2017-03-15 12:07:48 -0400326 LOG.debug(message)
327 raise rbac_exceptions.RbacParsingException(message)
Samantha Blanco0d880082017-03-23 18:14:37 -0400328 else:
329 rule = self.rules[apply_rule]
330 return rule(target, access_data, o)