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