1# 2# Licensed under the Apache License, Version 2.0 (the "License"); you may 3# not use this file except in compliance with the License. You may obtain 4# a copy of the License at 5# 6# http://www.apache.org/licenses/LICENSE-2.0 7# 8# Unless required by applicable law or agreed to in writing, software 9# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 10# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 11# License for the specific language governing permissions and limitations 12# under the License. 13 14"""Policy engine for openstack_auth""" 15 16import logging 17import os.path 18 19from django.conf import settings 20from oslo_config import cfg 21from oslo_policy import opts as policy_opts 22from oslo_policy import policy 23import yaml 24 25from openstack_auth import user as auth_user 26from openstack_auth import utils as auth_utils 27 28LOG = logging.getLogger(__name__) 29 30_ENFORCER = None 31_BASE_PATH = settings.POLICY_FILES_PATH 32 33 34def _get_policy_conf(policy_file, policy_dirs=None): 35 conf = cfg.ConfigOpts() 36 # Passing [] is required. Otherwise oslo.config looks up sys.argv. 37 conf([]) 38 policy_opts.set_defaults(conf) 39 conf.set_default('policy_file', policy_file, 'oslo_policy') 40 # Policy Enforcer has been updated to take in a policy directory 41 # as a config option. However, the default value in is set to 42 # ['policy.d'] which causes the code to break. Set the default 43 # value to empty list for now. 44 if policy_dirs is None: 45 policy_dirs = [] 46 conf.set_default('policy_dirs', policy_dirs, 'oslo_policy') 47 return conf 48 49 50def _get_policy_file_with_full_path(service): 51 policy_files = settings.POLICY_FILES 52 policy_file = os.path.join(_BASE_PATH, policy_files[service]) 53 policy_dirs = settings.POLICY_DIRS.get(service, []) 54 policy_dirs = [os.path.join(_BASE_PATH, policy_dir) 55 for policy_dir in policy_dirs] 56 return policy_file, policy_dirs 57 58 59def _convert_to_ruledefault(p): 60 deprecated = p.get('deprecated_rule') 61 if deprecated: 62 deprecated_rule = policy.DeprecatedRule(deprecated['name'], 63 deprecated['check_str']) 64 else: 65 deprecated_rule = None 66 67 return policy.RuleDefault( 68 p['name'], p['check_str'], 69 description=p['description'], 70 scope_types=p['scope_types'], 71 deprecated_rule=deprecated_rule, 72 deprecated_for_removal=p.get('deprecated_for_removal', False), 73 deprecated_reason=p.get('deprecated_reason'), 74 deprecated_since=p.get('deprecated_since'), 75 ) 76 77 78def _load_default_rules(service, enforcer): 79 policy_files = settings.DEFAULT_POLICY_FILES 80 try: 81 policy_file = os.path.join(_BASE_PATH, policy_files[service]) 82 except KeyError: 83 LOG.error('Default policy file for %s is not defined. ' 84 'Check DEFAULT_POLICY_FILES setting.', service) 85 return 86 87 try: 88 with open(policy_file) as f: 89 policies = yaml.safe_load(f) 90 except IOError as e: 91 LOG.error('Failed to open the policy file for %(service)s %(path)s: ' 92 '%(reason)s', 93 {'service': service, 'path': policy_file, 'reason': e}) 94 return 95 except yaml.YAMLError as e: 96 LOG.error('Failed to load the default policies for %(service)s: ' 97 '%(reason)s', {'service': service, 'reason': e}) 98 return 99 100 defaults = [_convert_to_ruledefault(p) for p in policies] 101 enforcer.register_defaults(defaults) 102 103 104def _get_enforcer(): 105 global _ENFORCER 106 if not _ENFORCER: 107 _ENFORCER = {} 108 policy_files = settings.POLICY_FILES 109 for service in policy_files.keys(): 110 policy_file, policy_dirs = _get_policy_file_with_full_path(service) 111 conf = _get_policy_conf(policy_file, policy_dirs) 112 enforcer = policy.Enforcer(conf) 113 enforcer.suppress_default_change_warnings = True 114 _load_default_rules(service, enforcer) 115 try: 116 enforcer.load_rules() 117 except IOError: 118 # Just in case if we have permission denied error which is not 119 # handled by oslo.policy now. It will handled in the code like 120 # we don't have any policy file: allow action from the Horizon 121 # side. 122 LOG.warning("Cannot load a policy file '%s' for service '%s' " 123 "due to IOError. One possible reason is " 124 "permission denied.", policy_file, service) 125 except ValueError: 126 LOG.warning("Cannot load a policy file '%s' for service '%s' " 127 "due to ValueError. The file might be wrongly " 128 "formatted.", policy_file, service) 129 130 # Ensure enforcer.rules is populated. 131 if enforcer.rules: 132 LOG.debug("adding enforcer for service: %s", service) 133 _ENFORCER[service] = enforcer 134 else: 135 locations = policy_file 136 if policy_dirs: 137 locations += ' and files under %s' % policy_dirs 138 LOG.warning("No policy rules for service '%s' in %s", 139 service, locations) 140 return _ENFORCER 141 142 143def reset(): 144 global _ENFORCER 145 _ENFORCER = None 146 147 148def check(actions, request, target=None): 149 """Check user permission. 150 151 Check if the user has permission to the action according 152 to policy setting. 153 154 :param actions: list of scope and action to do policy checks on, 155 the composition of which is (scope, action). Multiple actions 156 are treated as a logical AND. 157 158 * scope: service type managing the policy for action 159 160 * action: string representing the action to be checked 161 162 this should be colon separated for clarity. 163 i.e. 164 165 | compute:create_instance 166 | compute:attach_volume 167 | volume:attach_volume 168 169 for a policy action that requires a single action, actions 170 should look like 171 172 | "(("compute", "compute:create_instance"),)" 173 174 for a multiple action check, actions should look like 175 | "(("identity", "identity:list_users"), 176 | ("identity", "identity:list_roles"))" 177 178 :param request: django http request object. If not specified, credentials 179 must be passed. 180 :param target: dictionary representing the object of the action 181 for object creation this should be a dictionary 182 representing the location of the object e.g. 183 {'project_id': object.project_id} 184 :returns: boolean if the user has permission or not for the actions. 185 """ 186 if target is None: 187 target = {} 188 user = auth_utils.get_user(request) 189 190 # Several service policy engines default to a project id check for 191 # ownership. Since the user is already scoped to a project, if a 192 # different project id has not been specified use the currently scoped 193 # project's id. 194 # 195 # The reason is the operator can edit the local copies of the service 196 # policy file. If a rule is removed, then the default rule is used. We 197 # don't want to block all actions because the operator did not fully 198 # understand the implication of editing the policy file. Additionally, 199 # the service APIs will correct us if we are too permissive. 200 if target.get('project_id') is None: 201 target['project_id'] = user.project_id 202 if target.get('tenant_id') is None: 203 target['tenant_id'] = target['project_id'] 204 # same for user_id 205 if target.get('user_id') is None: 206 target['user_id'] = user.id 207 208 domain_id_keys = [ 209 'domain_id', 210 'project.domain_id', 211 'user.domain_id', 212 'group.domain_id' 213 ] 214 # populates domain id keys with user's current domain id 215 for key in domain_id_keys: 216 if target.get(key) is None: 217 target[key] = user.user_domain_id 218 219 credentials = _user_to_credentials(user) 220 domain_credentials = _domain_to_credentials(request, user) 221 # if there is a domain token use the domain_id instead of the user's domain 222 if domain_credentials: 223 credentials['domain_id'] = domain_credentials.get('domain_id') 224 225 enforcer = _get_enforcer() 226 227 for action in actions: 228 scope, action = action[0], action[1] 229 if scope in enforcer: 230 # this is for handling the v3 policy file and will only be 231 # needed when a domain scoped token is present 232 if scope == 'identity' and domain_credentials: 233 # use domain credentials 234 if not _check_credentials(enforcer[scope], 235 action, 236 target, 237 domain_credentials): 238 return False 239 240 # use project credentials 241 if not _check_credentials(enforcer[scope], 242 action, target, credentials): 243 return False 244 245 # if no policy for scope, allow action, underlying API will 246 # ultimately block the action if not permitted, treat as though 247 # allowed 248 return True 249 250 251def _check_credentials(enforcer_scope, action, target, credentials): 252 is_valid = True 253 if not enforcer_scope.enforce(action, target, credentials): 254 # to match service implementations, if a rule is not found, 255 # use the default rule for that service policy 256 # 257 # waiting to make the check because the first call to 258 # enforce loads the rules 259 if action not in enforcer_scope.rules: 260 if not enforcer_scope.enforce('default', target, credentials): 261 if 'default' in enforcer_scope.rules: 262 is_valid = False 263 else: 264 is_valid = False 265 return is_valid 266 267 268def _user_to_credentials(user): 269 if not hasattr(user, "_credentials"): 270 roles = [role['name'] for role in user.roles] 271 user._credentials = {'user_id': user.id, 272 'username': user.username, 273 'project_id': user.project_id, 274 'tenant_id': user.project_id, 275 'project_name': user.project_name, 276 'domain_id': user.user_domain_id, 277 'is_admin': user.is_superuser, 278 'roles': roles} 279 return user._credentials 280 281 282def _domain_to_credentials(request, user): 283 if not hasattr(user, "_domain_credentials"): 284 try: 285 domain_auth_ref = request.session.get('domain_token') 286 287 # no domain role or not running on V3 288 if not domain_auth_ref: 289 return None 290 domain_user = auth_user.create_user_from_token( 291 request, auth_user.Token(domain_auth_ref), 292 domain_auth_ref.service_catalog.url_for(interface=None)) 293 user._domain_credentials = _user_to_credentials(domain_user) 294 295 # uses the domain_id associated with the domain_user 296 user._domain_credentials['domain_id'] = domain_user.domain_id 297 298 except Exception: 299 LOG.warning("Failed to create user from domain scoped token.") 300 return None 301 return user._domain_credentials 302