~zulcss/ubuntu/precise/quantum/trunk

« back to all changes in this revision

Viewing changes to quantum/openstack/common/policy.py

  • Committer: Chuck Short
  • Date: 2012-11-26 19:51:11 UTC
  • mfrom: (26.1.1 raring-proposed)
  • Revision ID: zulcss@ubuntu.com-20121126195111-jnz2cr4xi6whemw2
* New upstream release for the Ubuntu Cloud Archive.
* debian/patches/*: Refreshed for opening of Grizzly.
* New upstream release.
* debian/rules: FTFBS if there is missing binaries.
* debian/quantum-server.install: Add quantum-debug.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
1
# vim: tabstop=4 shiftwidth=4 softtabstop=4
2
2
 
3
 
# Copyright (c) 2011 OpenStack, LLC.
 
3
# Copyright (c) 2012 OpenStack, LLC.
4
4
# All Rights Reserved.
5
5
#
6
6
#    Licensed under the Apache License, Version 2.0 (the "License"); you may
15
15
#    License for the specific language governing permissions and limitations
16
16
#    under the License.
17
17
 
18
 
"""Common Policy Engine Implementation"""
19
 
 
 
18
"""
 
19
Common Policy Engine Implementation
 
20
 
 
21
Policies can be expressed in one of two forms: A list of lists, or a
 
22
string written in the new policy language.
 
23
 
 
24
In the list-of-lists representation, each check inside the innermost
 
25
list is combined as with an "and" conjunction--for that check to pass,
 
26
all the specified checks must pass.  These innermost lists are then
 
27
combined as with an "or" conjunction.  This is the original way of
 
28
expressing policies, but there now exists a new way: the policy
 
29
language.
 
30
 
 
31
In the policy language, each check is specified the same way as in the
 
32
list-of-lists representation: a simple "a:b" pair that is matched to
 
33
the correct code to perform that check.  However, conjunction
 
34
operators are available, allowing for more expressiveness in crafting
 
35
policies.
 
36
 
 
37
As an example, take the following rule, expressed in the list-of-lists
 
38
representation::
 
39
 
 
40
    [["role:admin"], ["project_id:%(project_id)s", "role:projectadmin"]]
 
41
 
 
42
In the policy language, this becomes::
 
43
 
 
44
    role:admin or (project_id:%(project_id)s and role:projectadmin)
 
45
 
 
46
The policy language also has the "not" operator, allowing a richer
 
47
policy rule::
 
48
 
 
49
    project_id:%(project_id)s and not role:dunce
 
50
 
 
51
Finally, two special policy checks should be mentioned; the policy
 
52
check "@" will always accept an access, and the policy check "!" will
 
53
always reject an access.  (Note that if a rule is either the empty
 
54
list ("[]") or the empty string, this is equivalent to the "@" policy
 
55
check.)  Of these, the "!" policy check is probably the most useful,
 
56
as it allows particular rules to be explicitly disabled.
 
57
"""
 
58
 
 
59
import abc
20
60
import logging
 
61
import re
21
62
import urllib
 
63
 
22
64
import urllib2
23
65
 
24
66
from quantum.openstack.common.gettextutils import _
28
70
LOG = logging.getLogger(__name__)
29
71
 
30
72
 
31
 
_BRAIN = None
32
 
 
33
 
 
34
 
def set_brain(brain):
35
 
    """Set the brain used by enforce().
36
 
 
37
 
    Defaults use Brain() if not set.
38
 
 
39
 
    """
40
 
    global _BRAIN
41
 
    _BRAIN = brain
42
 
 
43
 
 
44
 
def reset():
45
 
    """Clear the brain used by enforce()."""
46
 
    global _BRAIN
47
 
    _BRAIN = None
48
 
 
49
 
 
50
 
def enforce(match_list, target_dict, credentials_dict, exc=None,
51
 
            *args, **kwargs):
52
 
    """Enforces authorization of some rules against credentials.
53
 
 
54
 
    :param match_list: nested tuples of data to match against
55
 
 
56
 
        The basic brain supports three types of match lists:
57
 
 
58
 
            1) rules
59
 
 
60
 
                looks like: ``('rule:compute:get_instance',)``
61
 
 
62
 
                Retrieves the named rule from the rules dict and recursively
63
 
                checks against the contents of the rule.
64
 
 
65
 
            2) roles
66
 
 
67
 
                looks like: ``('role:compute:admin',)``
68
 
 
69
 
                Matches if the specified role is in credentials_dict['roles'].
70
 
 
71
 
            3) generic
72
 
 
73
 
                looks like: ``('tenant_id:%(tenant_id)s',)``
74
 
 
75
 
                Substitutes values from the target dict into the match using
76
 
                the % operator and matches them against the creds dict.
77
 
 
78
 
        Combining rules:
79
 
 
80
 
            The brain returns True if any of the outer tuple of rules
81
 
            match and also True if all of the inner tuples match. You
82
 
            can use this to perform simple boolean logic.  For
83
 
            example, the following rule would return True if the creds
84
 
            contain the role 'admin' OR the if the tenant_id matches
85
 
            the target dict AND the the creds contains the role
86
 
            'compute_sysadmin':
87
 
 
88
 
            ::
89
 
 
90
 
                {
91
 
                    "rule:combined": (
92
 
                        'role:admin',
93
 
                        ('tenant_id:%(tenant_id)s', 'role:compute_sysadmin')
94
 
                    )
95
 
                }
96
 
 
97
 
        Note that rule and role are reserved words in the credentials match, so
98
 
        you can't match against properties with those names. Custom brains may
99
 
        also add new reserved words. For example, the HttpBrain adds http as a
100
 
        reserved word.
101
 
 
102
 
    :param target_dict: dict of object properties
103
 
 
104
 
      Target dicts contain as much information as we can about the object being
105
 
      operated on.
106
 
 
107
 
    :param credentials_dict: dict of actor properties
108
 
 
109
 
      Credentials dicts contain as much information as we can about the user
110
 
      performing the action.
111
 
 
112
 
    :param exc: exception to raise
113
 
 
114
 
      Class of the exception to raise if the check fails.  Any remaining
115
 
      arguments passed to enforce() (both positional and keyword arguments)
116
 
      will be passed to the exception class.  If exc is not provided, returns
117
 
      False.
118
 
 
119
 
    :return: True if the policy allows the action
120
 
    :return: False if the policy does not allow the action and exc is not set
121
 
    """
122
 
    global _BRAIN
123
 
    if not _BRAIN:
124
 
        _BRAIN = Brain()
125
 
    if not _BRAIN.check(match_list, target_dict, credentials_dict):
126
 
        if exc:
127
 
            raise exc(*args, **kwargs)
128
 
        return False
129
 
    return True
130
 
 
131
 
 
132
 
class Brain(object):
133
 
    """Implements policy checking."""
134
 
 
135
 
    _checks = {}
136
 
 
137
 
    @classmethod
138
 
    def _register(cls, name, func):
139
 
        cls._checks[name] = func
 
73
_rules = None
 
74
_checks = {}
 
75
 
 
76
 
 
77
class Rules(dict):
 
78
    """
 
79
    A store for rules.  Handles the default_rule setting directly.
 
80
    """
140
81
 
141
82
    @classmethod
142
83
    def load_json(cls, data, default_rule=None):
143
 
        """Init a brain using json instead of a rules dictionary."""
144
 
        rules_dict = jsonutils.loads(data)
145
 
        return cls(rules=rules_dict, default_rule=default_rule)
 
84
        """
 
85
        Allow loading of JSON rule data.
 
86
        """
 
87
 
 
88
        # Suck in the JSON data and parse the rules
 
89
        rules = dict((k, parse_rule(v)) for k, v in
 
90
                     jsonutils.loads(data).items())
 
91
 
 
92
        return cls(rules, default_rule)
146
93
 
147
94
    def __init__(self, rules=None, default_rule=None):
148
 
        if self.__class__ != Brain:
149
 
            LOG.warning(_("Inheritance-based rules are deprecated; use "
150
 
                          "the default brain instead of %s.") %
151
 
                        self.__class__.__name__)
 
95
        """Initialize the Rules store."""
152
96
 
153
 
        self.rules = rules or {}
 
97
        super(Rules, self).__init__(rules or {})
154
98
        self.default_rule = default_rule
155
99
 
156
 
    def add_rule(self, key, match):
157
 
        self.rules[key] = match
158
 
 
159
 
    def _check(self, match, target_dict, cred_dict):
160
 
        try:
161
 
            match_kind, match_value = match.split(':', 1)
162
 
        except Exception:
163
 
            LOG.exception(_("Failed to understand rule %(match)r") % locals())
164
 
            # If the rule is invalid, fail closed
165
 
            return False
166
 
 
167
 
        func = None
168
 
        try:
169
 
            old_func = getattr(self, '_check_%s' % match_kind)
170
 
        except AttributeError:
171
 
            func = self._checks.get(match_kind, self._checks.get(None, None))
172
 
        else:
173
 
            LOG.warning(_("Inheritance-based rules are deprecated; update "
174
 
                          "_check_%s") % match_kind)
175
 
            func = (lambda brain, kind, value, target, cred:
176
 
                        old_func(value, target, cred))
177
 
 
178
 
        if not func:
179
 
            LOG.error(_("No handler for matches of kind %s") % match_kind)
180
 
            # Fail closed
181
 
            return False
182
 
 
183
 
        return func(self, match_kind, match_value, target_dict, cred_dict)
184
 
 
185
 
    def check(self, match_list, target_dict, cred_dict):
186
 
        """Checks authorization of some rules against credentials.
187
 
 
188
 
        Detailed description of the check with examples in policy.enforce().
189
 
 
190
 
        :param match_list: nested tuples of data to match against
191
 
        :param target_dict: dict of object properties
192
 
        :param credentials_dict: dict of actor properties
193
 
 
194
 
        :returns: True if the check passes
195
 
 
196
 
        """
197
 
        if not match_list:
198
 
            return True
199
 
        for and_list in match_list:
200
 
            if isinstance(and_list, basestring):
201
 
                and_list = (and_list,)
202
 
            if all([self._check(item, target_dict, cred_dict)
203
 
                    for item in and_list]):
 
100
    def __missing__(self, key):
 
101
        """Implements the default rule handling."""
 
102
 
 
103
        # If the default rule isn't actually defined, do something
 
104
        # reasonably intelligent
 
105
        if not self.default_rule or self.default_rule not in self:
 
106
            raise KeyError(key)
 
107
 
 
108
        return self[self.default_rule]
 
109
 
 
110
    def __str__(self):
 
111
        """Dumps a string representation of the rules."""
 
112
 
 
113
        # Start by building the canonical strings for the rules
 
114
        out_rules = {}
 
115
        for key, value in self.items():
 
116
            # Use empty string for singleton TrueCheck instances
 
117
            if isinstance(value, TrueCheck):
 
118
                out_rules[key] = ''
 
119
            else:
 
120
                out_rules[key] = str(value)
 
121
 
 
122
        # Dump a pretty-printed JSON representation
 
123
        return jsonutils.dumps(out_rules, indent=4)
 
124
 
 
125
 
 
126
# Really have to figure out a way to deprecate this
 
127
def set_rules(rules):
 
128
    """Set the rules in use for policy checks."""
 
129
 
 
130
    global _rules
 
131
 
 
132
    _rules = rules
 
133
 
 
134
 
 
135
# Ditto
 
136
def reset():
 
137
    """Clear the rules used for policy checks."""
 
138
 
 
139
    global _rules
 
140
 
 
141
    _rules = None
 
142
 
 
143
 
 
144
def check(rule, target, creds, exc=None, *args, **kwargs):
 
145
    """
 
146
    Checks authorization of a rule against the target and credentials.
 
147
 
 
148
    :param rule: The rule to evaluate.
 
149
    :param target: As much information about the object being operated
 
150
                   on as possible, as a dictionary.
 
151
    :param creds: As much information about the user performing the
 
152
                  action as possible, as a dictionary.
 
153
    :param exc: Class of the exception to raise if the check fails.
 
154
                Any remaining arguments passed to check() (both
 
155
                positional and keyword arguments) will be passed to
 
156
                the exception class.  If exc is not provided, returns
 
157
                False.
 
158
 
 
159
    :return: Returns False if the policy does not allow the action and
 
160
             exc is not provided; otherwise, returns a value that
 
161
             evaluates to True.  Note: for rules using the "case"
 
162
             expression, this True value will be the specified string
 
163
             from the expression.
 
164
    """
 
165
 
 
166
    # Allow the rule to be a Check tree
 
167
    if isinstance(rule, BaseCheck):
 
168
        result = rule(target, creds)
 
169
    elif not _rules:
 
170
        # No rules to reference means we're going to fail closed
 
171
        result = False
 
172
    else:
 
173
        try:
 
174
            # Evaluate the rule
 
175
            result = _rules[rule](target, creds)
 
176
        except KeyError:
 
177
            # If the rule doesn't exist, fail closed
 
178
            result = False
 
179
 
 
180
    # If it is False, raise the exception if requested
 
181
    if exc and result is False:
 
182
        raise exc(*args, **kwargs)
 
183
 
 
184
    return result
 
185
 
 
186
 
 
187
class BaseCheck(object):
 
188
    """
 
189
    Abstract base class for Check classes.
 
190
    """
 
191
 
 
192
    __metaclass__ = abc.ABCMeta
 
193
 
 
194
    @abc.abstractmethod
 
195
    def __str__(self):
 
196
        """
 
197
        Retrieve a string representation of the Check tree rooted at
 
198
        this node.
 
199
        """
 
200
 
 
201
        pass
 
202
 
 
203
    @abc.abstractmethod
 
204
    def __call__(self, target, cred):
 
205
        """
 
206
        Perform the check.  Returns False to reject the access or a
 
207
        true value (not necessary True) to accept the access.
 
208
        """
 
209
 
 
210
        pass
 
211
 
 
212
 
 
213
class FalseCheck(BaseCheck):
 
214
    """
 
215
    A policy check that always returns False (disallow).
 
216
    """
 
217
 
 
218
    def __str__(self):
 
219
        """Return a string representation of this check."""
 
220
 
 
221
        return "!"
 
222
 
 
223
    def __call__(self, target, cred):
 
224
        """Check the policy."""
 
225
 
 
226
        return False
 
227
 
 
228
 
 
229
class TrueCheck(BaseCheck):
 
230
    """
 
231
    A policy check that always returns True (allow).
 
232
    """
 
233
 
 
234
    def __str__(self):
 
235
        """Return a string representation of this check."""
 
236
 
 
237
        return "@"
 
238
 
 
239
    def __call__(self, target, cred):
 
240
        """Check the policy."""
 
241
 
 
242
        return True
 
243
 
 
244
 
 
245
class Check(BaseCheck):
 
246
    """
 
247
    A base class to allow for user-defined policy checks.
 
248
    """
 
249
 
 
250
    def __init__(self, kind, match):
 
251
        """
 
252
        :param kind: The kind of the check, i.e., the field before the
 
253
                     ':'.
 
254
        :param match: The match of the check, i.e., the field after
 
255
                      the ':'.
 
256
        """
 
257
 
 
258
        self.kind = kind
 
259
        self.match = match
 
260
 
 
261
    def __str__(self):
 
262
        """Return a string representation of this check."""
 
263
 
 
264
        return "%s:%s" % (self.kind, self.match)
 
265
 
 
266
 
 
267
class NotCheck(BaseCheck):
 
268
    """
 
269
    A policy check that inverts the result of another policy check.
 
270
    Implements the "not" operator.
 
271
    """
 
272
 
 
273
    def __init__(self, rule):
 
274
        """
 
275
        Initialize the 'not' check.
 
276
 
 
277
        :param rule: The rule to negate.  Must be a Check.
 
278
        """
 
279
 
 
280
        self.rule = rule
 
281
 
 
282
    def __str__(self):
 
283
        """Return a string representation of this check."""
 
284
 
 
285
        return "not %s" % self.rule
 
286
 
 
287
    def __call__(self, target, cred):
 
288
        """
 
289
        Check the policy.  Returns the logical inverse of the wrapped
 
290
        check.
 
291
        """
 
292
 
 
293
        return not self.rule(target, cred)
 
294
 
 
295
 
 
296
class AndCheck(BaseCheck):
 
297
    """
 
298
    A policy check that requires that a list of other checks all
 
299
    return True.  Implements the "and" operator.
 
300
    """
 
301
 
 
302
    def __init__(self, rules):
 
303
        """
 
304
        Initialize the 'and' check.
 
305
 
 
306
        :param rules: A list of rules that will be tested.
 
307
        """
 
308
 
 
309
        self.rules = rules
 
310
 
 
311
    def __str__(self):
 
312
        """Return a string representation of this check."""
 
313
 
 
314
        return "(%s)" % ' and '.join(str(r) for r in self.rules)
 
315
 
 
316
    def __call__(self, target, cred):
 
317
        """
 
318
        Check the policy.  Requires that all rules accept in order to
 
319
        return True.
 
320
        """
 
321
 
 
322
        for rule in self.rules:
 
323
            if not rule(target, cred):
 
324
                return False
 
325
 
 
326
        return True
 
327
 
 
328
    def add_check(self, rule):
 
329
        """
 
330
        Allows addition of another rule to the list of rules that will
 
331
        be tested.  Returns the AndCheck object for convenience.
 
332
        """
 
333
 
 
334
        self.rules.append(rule)
 
335
        return self
 
336
 
 
337
 
 
338
class OrCheck(BaseCheck):
 
339
    """
 
340
    A policy check that requires that at least one of a list of other
 
341
    checks returns True.  Implements the "or" operator.
 
342
    """
 
343
 
 
344
    def __init__(self, rules):
 
345
        """
 
346
        Initialize the 'or' check.
 
347
 
 
348
        :param rules: A list of rules that will be tested.
 
349
        """
 
350
 
 
351
        self.rules = rules
 
352
 
 
353
    def __str__(self):
 
354
        """Return a string representation of this check."""
 
355
 
 
356
        return "(%s)" % ' or '.join(str(r) for r in self.rules)
 
357
 
 
358
    def __call__(self, target, cred):
 
359
        """
 
360
        Check the policy.  Requires that at least one rule accept in
 
361
        order to return True.
 
362
        """
 
363
 
 
364
        for rule in self.rules:
 
365
            if rule(target, cred):
204
366
                return True
 
367
 
205
368
        return False
206
369
 
207
 
 
208
 
class HttpBrain(Brain):
209
 
    """A brain that can check external urls for policy.
210
 
 
211
 
    Posts json blobs for target and credentials.
212
 
 
213
 
    Note that this brain is deprecated; the http check is registered
214
 
    by default.
215
 
    """
216
 
 
217
 
    pass
 
370
    def add_check(self, rule):
 
371
        """
 
372
        Allows addition of another rule to the list of rules that will
 
373
        be tested.  Returns the OrCheck object for convenience.
 
374
        """
 
375
 
 
376
        self.rules.append(rule)
 
377
        return self
 
378
 
 
379
 
 
380
def _parse_check(rule):
 
381
    """
 
382
    Parse a single base check rule into an appropriate Check object.
 
383
    """
 
384
 
 
385
    # Handle the special checks
 
386
    if rule == '!':
 
387
        return FalseCheck()
 
388
    elif rule == '@':
 
389
        return TrueCheck()
 
390
 
 
391
    try:
 
392
        kind, match = rule.split(':', 1)
 
393
    except Exception:
 
394
        LOG.exception(_("Failed to understand rule %(rule)s") % locals())
 
395
        # If the rule is invalid, we'll fail closed
 
396
        return FalseCheck()
 
397
 
 
398
    # Find what implements the check
 
399
    if kind in _checks:
 
400
        return _checks[kind](kind, match)
 
401
    elif None in _checks:
 
402
        return _checks[None](kind, match)
 
403
    else:
 
404
        LOG.error(_("No handler for matches of kind %s") % kind)
 
405
        return FalseCheck()
 
406
 
 
407
 
 
408
def _parse_list_rule(rule):
 
409
    """
 
410
    Provided for backwards compatibility.  Translates the old
 
411
    list-of-lists syntax into a tree of Check objects.
 
412
    """
 
413
 
 
414
    # Empty rule defaults to True
 
415
    if not rule:
 
416
        return TrueCheck()
 
417
 
 
418
    # Outer list is joined by "or"; inner list by "and"
 
419
    or_list = []
 
420
    for inner_rule in rule:
 
421
        # Elide empty inner lists
 
422
        if not inner_rule:
 
423
            continue
 
424
 
 
425
        # Handle bare strings
 
426
        if isinstance(inner_rule, basestring):
 
427
            inner_rule = [inner_rule]
 
428
 
 
429
        # Parse the inner rules into Check objects
 
430
        and_list = [_parse_check(r) for r in inner_rule]
 
431
 
 
432
        # Append the appropriate check to the or_list
 
433
        if len(and_list) == 1:
 
434
            or_list.append(and_list[0])
 
435
        else:
 
436
            or_list.append(AndCheck(and_list))
 
437
 
 
438
    # If we have only one check, omit the "or"
 
439
    if len(or_list) == 0:
 
440
        return FalseCheck()
 
441
    elif len(or_list) == 1:
 
442
        return or_list[0]
 
443
 
 
444
    return OrCheck(or_list)
 
445
 
 
446
 
 
447
# Used for tokenizing the policy language
 
448
_tokenize_re = re.compile(r'\s+')
 
449
 
 
450
 
 
451
def _parse_tokenize(rule):
 
452
    """
 
453
    Tokenizer for the policy language.
 
454
 
 
455
    Most of the single-character tokens are specified in the
 
456
    _tokenize_re; however, parentheses need to be handled specially,
 
457
    because they can appear inside a check string.  Thankfully, those
 
458
    parentheses that appear inside a check string can never occur at
 
459
    the very beginning or end ("%(variable)s" is the correct syntax).
 
460
    """
 
461
 
 
462
    for tok in _tokenize_re.split(rule):
 
463
        # Skip empty tokens
 
464
        if not tok or tok.isspace():
 
465
            continue
 
466
 
 
467
        # Handle leading parens on the token
 
468
        clean = tok.lstrip('(')
 
469
        for i in range(len(tok) - len(clean)):
 
470
            yield '(', '('
 
471
 
 
472
        # If it was only parentheses, continue
 
473
        if not clean:
 
474
            continue
 
475
        else:
 
476
            tok = clean
 
477
 
 
478
        # Handle trailing parens on the token
 
479
        clean = tok.rstrip(')')
 
480
        trail = len(tok) - len(clean)
 
481
 
 
482
        # Yield the cleaned token
 
483
        lowered = clean.lower()
 
484
        if lowered in ('and', 'or', 'not'):
 
485
            # Special tokens
 
486
            yield lowered, clean
 
487
        elif clean:
 
488
            # Not a special token, but not composed solely of ')'
 
489
            if len(tok) >= 2 and ((tok[0], tok[-1]) in
 
490
                                  [('"', '"'), ("'", "'")]):
 
491
                # It's a quoted string
 
492
                yield 'string', tok[1:-1]
 
493
            else:
 
494
                yield 'check', _parse_check(clean)
 
495
 
 
496
        # Yield the trailing parens
 
497
        for i in range(trail):
 
498
            yield ')', ')'
 
499
 
 
500
 
 
501
class ParseStateMeta(type):
 
502
    """
 
503
    Metaclass for the ParseState class.  Facilitates identifying
 
504
    reduction methods.
 
505
    """
 
506
 
 
507
    def __new__(mcs, name, bases, cls_dict):
 
508
        """
 
509
        Create the class.  Injects the 'reducers' list, a list of
 
510
        tuples matching token sequences to the names of the
 
511
        corresponding reduction methods.
 
512
        """
 
513
 
 
514
        reducers = []
 
515
 
 
516
        for key, value in cls_dict.items():
 
517
            if not hasattr(value, 'reducers'):
 
518
                continue
 
519
            for reduction in value.reducers:
 
520
                reducers.append((reduction, key))
 
521
 
 
522
        cls_dict['reducers'] = reducers
 
523
 
 
524
        return super(ParseStateMeta, mcs).__new__(mcs, name, bases, cls_dict)
 
525
 
 
526
 
 
527
def reducer(*tokens):
 
528
    """
 
529
    Decorator for reduction methods.  Arguments are a sequence of
 
530
    tokens, in order, which should trigger running this reduction
 
531
    method.
 
532
    """
 
533
 
 
534
    def decorator(func):
 
535
        # Make sure we have a list of reducer sequences
 
536
        if not hasattr(func, 'reducers'):
 
537
            func.reducers = []
 
538
 
 
539
        # Add the tokens to the list of reducer sequences
 
540
        func.reducers.append(list(tokens))
 
541
 
 
542
        return func
 
543
 
 
544
    return decorator
 
545
 
 
546
 
 
547
class ParseState(object):
 
548
    """
 
549
    Implement the core of parsing the policy language.  Uses a greedy
 
550
    reduction algorithm to reduce a sequence of tokens into a single
 
551
    terminal, the value of which will be the root of the Check tree.
 
552
 
 
553
    Note: error reporting is rather lacking.  The best we can get with
 
554
    this parser formulation is an overall "parse failed" error.
 
555
    Fortunately, the policy language is simple enough that this
 
556
    shouldn't be that big a problem.
 
557
    """
 
558
 
 
559
    __metaclass__ = ParseStateMeta
 
560
 
 
561
    def __init__(self):
 
562
        """Initialize the ParseState."""
 
563
 
 
564
        self.tokens = []
 
565
        self.values = []
 
566
 
 
567
    def reduce(self):
 
568
        """
 
569
        Perform a greedy reduction of the token stream.  If a reducer
 
570
        method matches, it will be executed, then the reduce() method
 
571
        will be called recursively to search for any more possible
 
572
        reductions.
 
573
        """
 
574
 
 
575
        for reduction, methname in self.reducers:
 
576
            if (len(self.tokens) >= len(reduction) and
 
577
                self.tokens[-len(reduction):] == reduction):
 
578
                    # Get the reduction method
 
579
                    meth = getattr(self, methname)
 
580
 
 
581
                    # Reduce the token stream
 
582
                    results = meth(*self.values[-len(reduction):])
 
583
 
 
584
                    # Update the tokens and values
 
585
                    self.tokens[-len(reduction):] = [r[0] for r in results]
 
586
                    self.values[-len(reduction):] = [r[1] for r in results]
 
587
 
 
588
                    # Check for any more reductions
 
589
                    return self.reduce()
 
590
 
 
591
    def shift(self, tok, value):
 
592
        """Adds one more token to the state.  Calls reduce()."""
 
593
 
 
594
        self.tokens.append(tok)
 
595
        self.values.append(value)
 
596
 
 
597
        # Do a greedy reduce...
 
598
        self.reduce()
 
599
 
 
600
    @property
 
601
    def result(self):
 
602
        """
 
603
        Obtain the final result of the parse.  Raises ValueError if
 
604
        the parse failed to reduce to a single result.
 
605
        """
 
606
 
 
607
        if len(self.values) != 1:
 
608
            raise ValueError("Could not parse rule")
 
609
        return self.values[0]
 
610
 
 
611
    @reducer('(', 'check', ')')
 
612
    @reducer('(', 'and_expr', ')')
 
613
    @reducer('(', 'or_expr', ')')
 
614
    def _wrap_check(self, _p1, check, _p2):
 
615
        """Turn parenthesized expressions into a 'check' token."""
 
616
 
 
617
        return [('check', check)]
 
618
 
 
619
    @reducer('check', 'and', 'check')
 
620
    def _make_and_expr(self, check1, _and, check2):
 
621
        """
 
622
        Create an 'and_expr' from two checks joined by the 'and'
 
623
        operator.
 
624
        """
 
625
 
 
626
        return [('and_expr', AndCheck([check1, check2]))]
 
627
 
 
628
    @reducer('and_expr', 'and', 'check')
 
629
    def _extend_and_expr(self, and_expr, _and, check):
 
630
        """
 
631
        Extend an 'and_expr' by adding one more check.
 
632
        """
 
633
 
 
634
        return [('and_expr', and_expr.add_check(check))]
 
635
 
 
636
    @reducer('check', 'or', 'check')
 
637
    def _make_or_expr(self, check1, _or, check2):
 
638
        """
 
639
        Create an 'or_expr' from two checks joined by the 'or'
 
640
        operator.
 
641
        """
 
642
 
 
643
        return [('or_expr', OrCheck([check1, check2]))]
 
644
 
 
645
    @reducer('or_expr', 'or', 'check')
 
646
    def _extend_or_expr(self, or_expr, _or, check):
 
647
        """
 
648
        Extend an 'or_expr' by adding one more check.
 
649
        """
 
650
 
 
651
        return [('or_expr', or_expr.add_check(check))]
 
652
 
 
653
    @reducer('not', 'check')
 
654
    def _make_not_expr(self, _not, check):
 
655
        """Invert the result of another check."""
 
656
 
 
657
        return [('check', NotCheck(check))]
 
658
 
 
659
 
 
660
def _parse_text_rule(rule):
 
661
    """
 
662
    Translates a policy written in the policy language into a tree of
 
663
    Check objects.
 
664
    """
 
665
 
 
666
    # Empty rule means always accept
 
667
    if not rule:
 
668
        return TrueCheck()
 
669
 
 
670
    # Parse the token stream
 
671
    state = ParseState()
 
672
    for tok, value in _parse_tokenize(rule):
 
673
        state.shift(tok, value)
 
674
 
 
675
    try:
 
676
        return state.result
 
677
    except ValueError:
 
678
        # Couldn't parse the rule
 
679
        LOG.exception(_("Failed to understand rule %(rule)r") % locals())
 
680
 
 
681
        # Fail closed
 
682
        return FalseCheck()
 
683
 
 
684
 
 
685
def parse_rule(rule):
 
686
    """
 
687
    Parses a policy rule into a tree of Check objects.
 
688
    """
 
689
 
 
690
    # If the rule is a string, it's in the policy language
 
691
    if isinstance(rule, basestring):
 
692
        return _parse_text_rule(rule)
 
693
    return _parse_list_rule(rule)
218
694
 
219
695
 
220
696
def register(name, func=None):
221
697
    """
222
 
    Register a function as a policy check.
 
698
    Register a function or Check class as a policy check.
223
699
 
224
700
    :param name: Gives the name of the check type, e.g., 'rule',
225
 
                 'role', etc.  If name is None, a default function
 
701
                 'role', etc.  If name is None, a default check type
226
702
                 will be registered.
227
 
    :param func: If given, provides the function to register.  If not
228
 
                 given, returns a function taking one argument to
229
 
                 specify the function to register, allowing use as a
230
 
                 decorator.
 
703
    :param func: If given, provides the function or class to register.
 
704
                 If not given, returns a function taking one argument
 
705
                 to specify the function or class to register,
 
706
                 allowing use as a decorator.
231
707
    """
232
708
 
233
 
    # Perform the actual decoration by registering the function.
234
 
    # Returns the function for compliance with the decorator
235
 
    # interface.
 
709
    # Perform the actual decoration by registering the function or
 
710
    # class.  Returns the function or class for compliance with the
 
711
    # decorator interface.
236
712
    def decorator(func):
237
 
        # Register the function
238
 
        Brain._register(name, func)
 
713
        _checks[name] = func
239
714
        return func
240
715
 
241
 
    # If the function is given, do the registration
 
716
    # If the function or class is given, do the registration
242
717
    if func:
243
718
        return decorator(func)
244
719
 
246
721
 
247
722
 
248
723
@register("rule")
249
 
def _check_rule(brain, match_kind, match, target_dict, cred_dict):
250
 
    """Recursively checks credentials based on the brains rules."""
251
 
    try:
252
 
        new_match_list = brain.rules[match]
253
 
    except KeyError:
254
 
        if brain.default_rule and match != brain.default_rule:
255
 
            new_match_list = ('rule:%s' % brain.default_rule,)
256
 
        else:
 
724
class RuleCheck(Check):
 
725
    def __call__(self, target, creds):
 
726
        """
 
727
        Recursively checks credentials based on the defined rules.
 
728
        """
 
729
 
 
730
        try:
 
731
            return _rules[self.match](target, creds)
 
732
        except KeyError:
 
733
            # We don't have any matching rule; fail closed
257
734
            return False
258
735
 
259
 
    return brain.check(new_match_list, target_dict, cred_dict)
260
 
 
261
736
 
262
737
@register("role")
263
 
def _check_role(brain, match_kind, match, target_dict, cred_dict):
264
 
    """Check that there is a matching role in the cred dict."""
265
 
    return match.lower() in [x.lower() for x in cred_dict['roles']]
 
738
class RoleCheck(Check):
 
739
    def __call__(self, target, creds):
 
740
        """Check that there is a matching role in the cred dict."""
 
741
 
 
742
        return self.match.lower() in [x.lower() for x in creds['roles']]
266
743
 
267
744
 
268
745
@register('http')
269
 
def _check_http(brain, match_kind, match, target_dict, cred_dict):
270
 
    """Check http: rules by calling to a remote server.
271
 
 
272
 
    This example implementation simply verifies that the response is
273
 
    exactly 'True'. A custom brain using response codes could easily
274
 
    be implemented.
275
 
 
276
 
    """
277
 
    url = 'http:' + (match % target_dict)
278
 
    data = {'target': jsonutils.dumps(target_dict),
279
 
            'credentials': jsonutils.dumps(cred_dict)}
280
 
    post_data = urllib.urlencode(data)
281
 
    f = urllib2.urlopen(url, post_data)
282
 
    return f.read() == "True"
 
746
class HttpCheck(Check):
 
747
    def __call__(self, target, creds):
 
748
        """
 
749
        Check http: rules by calling to a remote server.
 
750
 
 
751
        This example implementation simply verifies that the response
 
752
        is exactly 'True'.
 
753
        """
 
754
 
 
755
        url = ('http:' + self.match) % target
 
756
        data = {'target': jsonutils.dumps(target),
 
757
                'credentials': jsonutils.dumps(creds)}
 
758
        post_data = urllib.urlencode(data)
 
759
        f = urllib2.urlopen(url, post_data)
 
760
        return f.read() == "True"
283
761
 
284
762
 
285
763
@register(None)
286
 
def _check_generic(brain, match_kind, match, target_dict, cred_dict):
287
 
    """Check an individual match.
288
 
 
289
 
    Matches look like:
290
 
 
291
 
        tenant:%(tenant_id)s
292
 
        role:compute:admin
293
 
 
294
 
    """
295
 
 
296
 
    # TODO(termie): do dict inspection via dot syntax
297
 
    match = match % target_dict
298
 
    if match_kind in cred_dict:
299
 
        return match == unicode(cred_dict[match_kind])
300
 
    return False
 
764
class GenericCheck(Check):
 
765
    def __call__(self, target, creds):
 
766
        """
 
767
        Check an individual match.
 
768
 
 
769
        Matches look like:
 
770
 
 
771
            tenant:%(tenant_id)s
 
772
            role:compute:admin
 
773
        """
 
774
 
 
775
        # TODO(termie): do dict inspection via dot syntax
 
776
        match = self.match % target
 
777
        if self.kind in creds:
 
778
            return match == unicode(creds[self.kind])
 
779
        return False