blob: 38bed7c465256b8de743e3515db3292270f279fd [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
DavidPurcell029d8c32017-01-06 15:27:41 -050028
Felipe Monteiroae2ebab2017-03-23 22:49:06 +000029CONF = cfg.CONF
DavidPurcell029d8c32017-01-06 15:27:41 -050030LOG = logging.getLogger(__name__)
31
DavidPurcell029d8c32017-01-06 15:27:41 -050032
Felipe Monteiro322c5b62017-02-26 02:44:21 +000033class RbacPolicyParser(object):
DavidPurcell029d8c32017-01-06 15:27:41 -050034 """A class for parsing policy rules into lists of allowed roles.
35
36 RBAC testing requires that each rule in a policy file be broken up into
37 the roles that constitute it. This class automates that process.
Felipe Monteirob0595652017-01-23 16:51:58 -050038
39 The list of roles per rule can be reverse-engineered by checking, for
40 each role, whether a given rule is allowed using oslo policy.
DavidPurcell029d8c32017-01-06 15:27:41 -050041 """
42
Felipe Monteiro889264e2017-03-01 17:19:35 -050043 def __init__(self, tenant_id, user_id, service=None, path=None):
Felipe Monteiro322c5b62017-02-26 02:44:21 +000044 """Initialization of Rbac Policy Parser.
DavidPurcell029d8c32017-01-06 15:27:41 -050045
Felipe Monteiro9c978502017-01-27 17:07:54 -050046 Parses a policy file to create a dictionary, mapping policy actions to
47 roles. If a policy file does not exist, checks whether the policy file
48 is registered as a namespace under oslo.policy.policies. Nova, for
49 example, doesn't use a policy.json file by default; its policy is
50 implemented in code and registered as 'nova' under
51 oslo.policy.policies.
52
53 If the policy file is not found in either place, raises an exception.
54
55 Additionally, if the policy file exists in both code and as a
56 policy.json (for example, by creating a custom nova policy.json file),
57 the custom policy file over the default policy implementation is
58 prioritized.
Felipe Monteirob0595652017-01-23 16:51:58 -050059
60 :param tenant_id: type uuid
Felipe Monteiro889264e2017-03-01 17:19:35 -050061 :param user_id: type uuid
DavidPurcell029d8c32017-01-06 15:27:41 -050062 :param service: type string
63 :param path: type string
64 """
Felipe Monteiroae2ebab2017-03-23 22:49:06 +000065
Rick Bartra503c5572017-03-09 13:49:58 -050066 # First check if the service is valid
67 service = service.lower().strip() if service else None
68 self.admin_mgr = credentials.AdminManager()
69 services = self.admin_mgr.identity_services_v3_client.\
70 list_services()['services']
71 service_names = [s['name'] for s in services]
72 if not service or not any(service in name for name in service_names):
73 LOG.debug(str(service) + " is NOT a valid service.")
74 raise rbac_exceptions.RbacInvalidService
75
Felipe Monteiroae2ebab2017-03-23 22:49:06 +000076 # Use default path in /etc/<service_name/policy.json if no path
77 # is provided.
78 self.path = path or os.path.join('/etc', service, 'policy.json')
79 self.rules = policy.Rules.load(self._get_policy_data(service),
80 'default')
Felipe Monteirob0595652017-01-23 16:51:58 -050081 self.tenant_id = tenant_id
Felipe Monteiro889264e2017-03-01 17:19:35 -050082 self.user_id = user_id
DavidPurcell029d8c32017-01-06 15:27:41 -050083
Felipe Monteirob0595652017-01-23 16:51:58 -050084 def allowed(self, rule_name, role):
Felipe Monteiro9c978502017-01-27 17:07:54 -050085 is_admin_context = self._is_admin_context(role)
Felipe Monteirob0595652017-01-23 16:51:58 -050086 is_allowed = self._allowed(
Felipe Monteiro9c978502017-01-27 17:07:54 -050087 access=self._get_access_token(role),
Felipe Monteirob0595652017-01-23 16:51:58 -050088 apply_rule=rule_name,
Felipe Monteiro9c978502017-01-27 17:07:54 -050089 is_admin=is_admin_context)
Felipe Monteiro9c978502017-01-27 17:07:54 -050090 return is_allowed
DavidPurcell029d8c32017-01-06 15:27:41 -050091
Felipe Monteiroae2ebab2017-03-23 22:49:06 +000092 def _get_policy_data(self, service):
93 file_policy_data = {}
94 mgr_policy_data = {}
95 policy_data = {}
96
97 # Check whether policy file exists.
98 if os.path.isfile(self.path):
99 with open(self.path, 'r') as policy_file:
100 file_policy_data = policy_file.read()
101 try:
102 file_policy_data = json.loads(file_policy_data)
103 except ValueError:
104 pass
105
106 # Check whether policy actions are defined in code. Nova and Keystone,
107 # for example, define their default policy actions in code.
108 mgr = stevedore.named.NamedExtensionManager(
109 'oslo.policy.policies',
110 names=[service],
111 on_load_failure_callback=None,
112 invoke_on_load=True,
113 warn_on_missing_entrypoint=False)
114
115 if mgr:
116 policy_generator = {policy.name: policy.obj for policy in mgr}
117 if policy_generator and service in policy_generator:
118 for rule in policy_generator[service]:
119 mgr_policy_data[rule.name] = str(rule.check)
120
121 # If data from both file and code exist, combine both together.
122 if file_policy_data and mgr_policy_data:
123 # Add the policy actions from code first.
124 for action, rule in mgr_policy_data.items():
125 policy_data[action] = rule
126 # Overwrite with any custom policy actions defined in policy.json.
127 for action, rule in file_policy_data.items():
128 policy_data[action] = rule
129 elif file_policy_data:
130 policy_data = file_policy_data
131 elif mgr_policy_data:
132 policy_data = mgr_policy_data
133 else:
134 error_message = 'Policy file for {0} service neither found in '\
135 'code nor at {1}.'.format(service, self.path)
136 raise rbac_exceptions.RbacParsingException(error_message)
137
138 try:
139 policy_data = json.dumps(policy_data)
140 except ValueError:
141 error_message = 'Policy file for {0} service is invalid.'.format(
142 service)
143 raise rbac_exceptions.RbacParsingException(error_message)
144
145 return policy_data
146
Felipe Monteiro9c978502017-01-27 17:07:54 -0500147 def _is_admin_context(self, role):
148 """Checks whether a role has admin context.
149
150 If context_is_admin is contained in the policy file, then checks
151 whether the given role is contained in context_is_admin. If it is not
152 in the policy file, then default to context_is_admin: admin.
153 """
154 if 'context_is_admin' in self.rules.keys():
155 return self._allowed(
156 access=self._get_access_token(role),
157 apply_rule='context_is_admin')
158 return role == 'admin'
DavidPurcell029d8c32017-01-06 15:27:41 -0500159
Felipe Monteirob0595652017-01-23 16:51:58 -0500160 def _get_access_token(self, role):
161 access_token = {
162 "token": {
163 "roles": [
164 {
165 "name": role
166 }
167 ],
Felipe Monteiro9fc782e2017-02-01 15:38:46 -0500168 "project_id": self.tenant_id,
Felipe Monteiro889264e2017-03-01 17:19:35 -0500169 "tenant_id": self.tenant_id,
170 "user_id": self.user_id
Felipe Monteirob0595652017-01-23 16:51:58 -0500171 }
172 }
173 return access_token
DavidPurcell029d8c32017-01-06 15:27:41 -0500174
Felipe Monteiro9c978502017-01-27 17:07:54 -0500175 def _allowed(self, access, apply_rule, is_admin=False):
Felipe Monteirob0595652017-01-23 16:51:58 -0500176 """Checks if a given rule in a policy is allowed with given access.
DavidPurcell029d8c32017-01-06 15:27:41 -0500177
Felipe Monteirob0595652017-01-23 16:51:58 -0500178 Adapted from oslo_policy.shell.
DavidPurcell029d8c32017-01-06 15:27:41 -0500179
Felipe Monteirob0595652017-01-23 16:51:58 -0500180 :param access: type dict: dictionary from ``_get_access_token``
181 :param apply_rule: type string: rule to be checked
182 :param is_admin: type bool: whether admin context is used
DavidPurcell029d8c32017-01-06 15:27:41 -0500183 """
Felipe Monteirob0595652017-01-23 16:51:58 -0500184 access_data = copy.copy(access['token'])
185 access_data['roles'] = [role['name'] for role in access_data['roles']]
Felipe Monteirob0595652017-01-23 16:51:58 -0500186 access_data['is_admin'] = is_admin
Felipe Monteiro9c978502017-01-27 17:07:54 -0500187 # TODO(felipemonteiro): Dynamically calculate is_admin_project rather
188 # than hard-coding it to True. is_admin_project cannot be determined
189 # from the role, but rather from project and domain names. See
190 # _populate_is_admin_project in keystone.token.providers.common
191 # for more information.
192 access_data['is_admin_project'] = True
DavidPurcell029d8c32017-01-06 15:27:41 -0500193
Felipe Monteirob0595652017-01-23 16:51:58 -0500194 class Object(object):
195 pass
196 o = Object()
Felipe Monteiro9c978502017-01-27 17:07:54 -0500197 o.rules = self.rules
DavidPurcell029d8c32017-01-06 15:27:41 -0500198
Felipe Monteiro9fc782e2017-02-01 15:38:46 -0500199 target = {"project_id": access_data['project_id'],
200 "tenant_id": access_data['project_id'],
Felipe Monteiro889264e2017-03-01 17:19:35 -0500201 "network:tenant_id": access_data['project_id'],
202 "user_id": access_data['user_id']}
Felipe Monteirob0595652017-01-23 16:51:58 -0500203
Felipe Monteiro9c978502017-01-27 17:07:54 -0500204 result = self._try_rule(apply_rule, target, access_data, o)
Felipe Monteirob0595652017-01-23 16:51:58 -0500205 return result
206
Felipe Monteiro9c978502017-01-27 17:07:54 -0500207 def _try_rule(self, apply_rule, target, access_data, o):
Samantha Blanco0d880082017-03-23 18:14:37 -0400208 if apply_rule not in self.rules:
Felipe Monteiro48c913d2017-03-15 12:07:48 -0400209 message = "Policy action: {0} not found in policy file: {1}."\
210 .format(apply_rule, self.path)
211 LOG.debug(message)
212 raise rbac_exceptions.RbacParsingException(message)
Samantha Blanco0d880082017-03-23 18:14:37 -0400213 else:
214 rule = self.rules[apply_rule]
215 return rule(target, access_data, o)