~ubuntu-branches/debian/sid/keystone/sid

« back to all changes in this revision

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

  • Committer: Package Import Robot
  • Author(s): Thomas Goirand
  • Date: 2013-05-10 10:22:18 UTC
  • mfrom: (1.2.1) (26.1.4 experimental)
  • Revision ID: package-import@ubuntu.com-20130510102218-7hph1420gz5jsyr7
Tags: 2013.1.1-2
* Uploading to unstable.
* New upstream release:
  - Fixes CVE-2013-2059: Keystone tokens not immediately invalidated when
  user is deleted [OSSA 2013-011] (Closes: #707598).
* Also installs httpd/keystone.py.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# vim: tabstop=4 shiftwidth=4 softtabstop=4
 
2
 
 
3
# Copyright (c) 2012 OpenStack, LLC.
 
4
# All Rights Reserved.
 
5
#
 
6
#    Licensed under the Apache License, Version 2.0 (the "License"); you may
 
7
#    not use this file except in compliance with the License. You may obtain
 
8
#    a copy of the License at
 
9
#
 
10
#         http://www.apache.org/licenses/LICENSE-2.0
 
11
#
 
12
#    Unless required by applicable law or agreed to in writing, software
 
13
#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 
14
#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 
15
#    License for the specific language governing permissions and limitations
 
16
#    under the License.
 
17
 
 
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
 
60
import logging
 
61
import re
 
62
import urllib
 
63
 
 
64
import urllib2
 
65
 
 
66
from keystone.openstack.common.gettextutils import _
 
67
from keystone.openstack.common import jsonutils
 
68
 
 
69
 
 
70
LOG = logging.getLogger(__name__)
 
71
 
 
72
 
 
73
_rules = None
 
74
_checks = {}
 
75
 
 
76
 
 
77
class Rules(dict):
 
78
    """
 
79
    A store for rules.  Handles the default_rule setting directly.
 
80
    """
 
81
 
 
82
    @classmethod
 
83
    def load_json(cls, data, default_rule=None):
 
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)
 
93
 
 
94
    def __init__(self, rules=None, default_rule=None):
 
95
        """Initialize the Rules store."""
 
96
 
 
97
        super(Rules, self).__init__(rules or {})
 
98
        self.default_rule = default_rule
 
99
 
 
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):
 
366
                return True
 
367
 
 
368
        return False
 
369
 
 
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)
 
694
 
 
695
 
 
696
def register(name, func=None):
 
697
    """
 
698
    Register a function or Check class as a policy check.
 
699
 
 
700
    :param name: Gives the name of the check type, e.g., 'rule',
 
701
                 'role', etc.  If name is None, a default check type
 
702
                 will be registered.
 
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.
 
707
    """
 
708
 
 
709
    # Perform the actual decoration by registering the function or
 
710
    # class.  Returns the function or class for compliance with the
 
711
    # decorator interface.
 
712
    def decorator(func):
 
713
        _checks[name] = func
 
714
        return func
 
715
 
 
716
    # If the function or class is given, do the registration
 
717
    if func:
 
718
        return decorator(func)
 
719
 
 
720
    return decorator
 
721
 
 
722
 
 
723
@register("rule")
 
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
 
734
            return False
 
735
 
 
736
 
 
737
@register("role")
 
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']]
 
743
 
 
744
 
 
745
@register('http')
 
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"
 
761
 
 
762
 
 
763
@register(None)
 
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