summaryrefslogtreecommitdiff
path: root/openstack_auth/policy.py
blob: 2b43ef3a519057a2d179d401cb397a52978a278f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
#
#    Licensed under the Apache License, Version 2.0 (the "License"); you may
#    not use this file except in compliance with the License. You may obtain
#    a copy of the License at
#
#         http://www.apache.org/licenses/LICENSE-2.0
#
#    Unless required by applicable law or agreed to in writing, software
#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
#    License for the specific language governing permissions and limitations
#    under the License.

"""Policy engine for openstack_auth"""

import logging
import os.path

from django.conf import settings
from oslo_config import cfg
from oslo_policy import opts as policy_opts
from oslo_policy import policy
import yaml

from openstack_auth import user as auth_user
from openstack_auth import utils as auth_utils

LOG = logging.getLogger(__name__)

_ENFORCER = None
_BASE_PATH = settings.POLICY_FILES_PATH


def _get_policy_conf(policy_file, policy_dirs=None):
    conf = cfg.ConfigOpts()
    # Passing [] is required. Otherwise oslo.config looks up sys.argv.
    conf([])
    policy_opts.set_defaults(conf)
    conf.set_default('policy_file', policy_file, 'oslo_policy')
    # Policy Enforcer has been updated to take in a policy directory
    # as a config option. However, the default value in is set to
    # ['policy.d'] which causes the code to break. Set the default
    # value to empty list for now.
    if policy_dirs is None:
        policy_dirs = []
    conf.set_default('policy_dirs', policy_dirs, 'oslo_policy')
    return conf


def _get_policy_file_with_full_path(service):
    policy_files = settings.POLICY_FILES
    policy_file = os.path.join(_BASE_PATH, policy_files[service])
    policy_dirs = settings.POLICY_DIRS.get(service, [])
    policy_dirs = [os.path.join(_BASE_PATH, policy_dir)
                   for policy_dir in policy_dirs]
    return policy_file, policy_dirs


def _convert_to_ruledefault(p):
    deprecated = p.get('deprecated_rule')
    if deprecated:
        deprecated_rule = policy.DeprecatedRule(deprecated['name'],
                                                deprecated['check_str'])
    else:
        deprecated_rule = None

    return policy.RuleDefault(
        p['name'], p['check_str'],
        description=p['description'],
        scope_types=p['scope_types'],
        deprecated_rule=deprecated_rule,
        deprecated_for_removal=p.get('deprecated_for_removal', False),
        deprecated_reason=p.get('deprecated_reason'),
        deprecated_since=p.get('deprecated_since'),
    )


def _load_default_rules(service, enforcer):
    policy_files = settings.DEFAULT_POLICY_FILES
    try:
        policy_file = os.path.join(_BASE_PATH, policy_files[service])
    except KeyError:
        LOG.error('Default policy file for %s is not defined. '
                  'Check DEFAULT_POLICY_FILES setting.', service)
        return

    try:
        with open(policy_file) as f:
            policies = yaml.safe_load(f)
    except IOError as e:
        LOG.error('Failed to open the policy file for %(service)s %(path)s: '
                  '%(reason)s',
                  {'service': service, 'path': policy_file, 'reason': e})
        return
    except yaml.YAMLError as e:
        LOG.error('Failed to load the default policies for %(service)s: '
                  '%(reason)s', {'service': service, 'reason': e})
        return

    defaults = [_convert_to_ruledefault(p) for p in policies]
    enforcer.register_defaults(defaults)


def _get_enforcer():
    global _ENFORCER
    if not _ENFORCER:
        _ENFORCER = {}
        policy_files = settings.POLICY_FILES
        for service in policy_files.keys():
            policy_file, policy_dirs = _get_policy_file_with_full_path(service)
            conf = _get_policy_conf(policy_file, policy_dirs)
            enforcer = policy.Enforcer(conf)
            enforcer.suppress_default_change_warnings = True
            _load_default_rules(service, enforcer)
            try:
                enforcer.load_rules()
            except IOError:
                # Just in case if we have permission denied error which is not
                # handled by oslo.policy now. It will handled in the code like
                # we don't have any policy file: allow action from the Horizon
                # side.
                LOG.warning("Cannot load a policy file '%s' for service '%s' "
                            "due to IOError. One possible reason is "
                            "permission denied.", policy_file, service)
            except ValueError:
                LOG.warning("Cannot load a policy file '%s' for service '%s' "
                            "due to ValueError. The file might be wrongly "
                            "formatted.", policy_file, service)

            # Ensure enforcer.rules is populated.
            if enforcer.rules:
                LOG.debug("adding enforcer for service: %s", service)
                _ENFORCER[service] = enforcer
            else:
                locations = policy_file
                if policy_dirs:
                    locations += ' and files under %s' % policy_dirs
                LOG.warning("No policy rules for service '%s' in %s",
                            service, locations)
    return _ENFORCER


def reset():
    global _ENFORCER
    _ENFORCER = None


def check(actions, request, target=None):
    """Check user permission.

    Check if the user has permission to the action according
    to policy setting.

    :param actions: list of scope and action to do policy checks on,
        the composition of which is (scope, action). Multiple actions
        are treated as a logical AND.

        * scope: service type managing the policy for action

        * action: string representing the action to be checked

            this should be colon separated for clarity.
            i.e.

                | compute:create_instance
                | compute:attach_volume
                | volume:attach_volume

        for a policy action that requires a single action, actions
        should look like

            | "(("compute", "compute:create_instance"),)"

        for a multiple action check, actions should look like
            | "(("identity", "identity:list_users"),
            |   ("identity", "identity:list_roles"))"

    :param request: django http request object. If not specified, credentials
                    must be passed.
    :param target: dictionary representing the object of the action
                      for object creation this should be a dictionary
                      representing the location of the object e.g.
                      {'project_id': object.project_id}
    :returns: boolean if the user has permission or not for the actions.
    """
    if target is None:
        target = {}
    user = auth_utils.get_user(request)

    # Several service policy engines default to a project id check for
    # ownership. Since the user is already scoped to a project, if a
    # different project id has not been specified use the currently scoped
    # project's id.
    #
    # The reason is the operator can edit the local copies of the service
    # policy file. If a rule is removed, then the default rule is used. We
    # don't want to block all actions because the operator did not fully
    # understand the implication of editing the policy file. Additionally,
    # the service APIs will correct us if we are too permissive.
    if target.get('project_id') is None:
        target['project_id'] = user.project_id
    if target.get('tenant_id') is None:
        target['tenant_id'] = target['project_id']
    # same for user_id
    if target.get('user_id') is None:
        target['user_id'] = user.id

    domain_id_keys = [
        'domain_id',
        'project.domain_id',
        'user.domain_id',
        'group.domain_id'
    ]
    # populates domain id keys with user's current domain id
    for key in domain_id_keys:
        if target.get(key) is None:
            target[key] = user.user_domain_id

    credentials = _user_to_credentials(user)
    domain_credentials = _domain_to_credentials(request, user)
    # if there is a domain token use the domain_id instead of the user's domain
    if domain_credentials:
        credentials['domain_id'] = domain_credentials.get('domain_id')

    enforcer = _get_enforcer()

    for action in actions:
        scope, action = action[0], action[1]
        if scope in enforcer:
            # this is for handling the v3 policy file and will only be
            # needed when a domain scoped token is present
            if scope == 'identity' and domain_credentials:
                # use domain credentials
                if not _check_credentials(enforcer[scope],
                                          action,
                                          target,
                                          domain_credentials):
                    return False

            # use project credentials
            if not _check_credentials(enforcer[scope],
                                      action, target, credentials):
                return False

        # if no policy for scope, allow action, underlying API will
        # ultimately block the action if not permitted, treat as though
        # allowed
    return True


def _check_credentials(enforcer_scope, action, target, credentials):
    is_valid = True
    if not enforcer_scope.enforce(action, target, credentials):
        # to match service implementations, if a rule is not found,
        # use the default rule for that service policy
        #
        # waiting to make the check because the first call to
        # enforce loads the rules
        if action not in enforcer_scope.rules:
            if not enforcer_scope.enforce('default', target, credentials):
                if 'default' in enforcer_scope.rules:
                    is_valid = False
        else:
            is_valid = False
    return is_valid


def _user_to_credentials(user):
    if not hasattr(user, "_credentials"):
        roles = [role['name'] for role in user.roles]
        user._credentials = {'user_id': user.id,
                             'username': user.username,
                             'project_id': user.project_id,
                             'tenant_id': user.project_id,
                             'project_name': user.project_name,
                             'domain_id': user.user_domain_id,
                             'is_admin': user.is_superuser,
                             'roles': roles}
    return user._credentials


def _domain_to_credentials(request, user):
    if not hasattr(user, "_domain_credentials"):
        try:
            domain_auth_ref = request.session.get('domain_token')

            # no domain role or not running on V3
            if not domain_auth_ref:
                return None
            domain_user = auth_user.create_user_from_token(
                request, auth_user.Token(domain_auth_ref),
                domain_auth_ref.service_catalog.url_for(interface=None))
            user._domain_credentials = _user_to_credentials(domain_user)

            # uses the domain_id associated with the domain_user
            user._domain_credentials['domain_id'] = domain_user.domain_id

        except Exception:
            LOG.warning("Failed to create user from domain scoped token.")
            return None
    return user._domain_credentials