1
# vim: tabstop=4 shiftwidth=4 softtabstop=4
3
# Copyright (c) 2012 OpenStack, LLC.
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
10
# http://www.apache.org/licenses/LICENSE-2.0
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
19
Common Policy Engine Implementation
21
Policies can be expressed in one of two forms: A list of lists, or a
22
string written in the new policy language.
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
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
37
As an example, take the following rule, expressed in the list-of-lists
40
[["role:admin"], ["project_id:%(project_id)s", "role:projectadmin"]]
42
In the policy language, this becomes::
44
role:admin or (project_id:%(project_id)s and role:projectadmin)
46
The policy language also has the "not" operator, allowing a richer
49
project_id:%(project_id)s and not role:dunce
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.
66
from keystone.openstack.common.gettextutils import _
67
from keystone.openstack.common import jsonutils
70
LOG = logging.getLogger(__name__)
79
A store for rules. Handles the default_rule setting directly.
83
def load_json(cls, data, default_rule=None):
85
Allow loading of JSON rule data.
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())
92
return cls(rules, default_rule)
94
def __init__(self, rules=None, default_rule=None):
95
"""Initialize the Rules store."""
97
super(Rules, self).__init__(rules or {})
98
self.default_rule = default_rule
100
def __missing__(self, key):
101
"""Implements the default rule handling."""
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:
108
return self[self.default_rule]
111
"""Dumps a string representation of the rules."""
113
# Start by building the canonical strings for the rules
115
for key, value in self.items():
116
# Use empty string for singleton TrueCheck instances
117
if isinstance(value, TrueCheck):
120
out_rules[key] = str(value)
122
# Dump a pretty-printed JSON representation
123
return jsonutils.dumps(out_rules, indent=4)
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."""
137
"""Clear the rules used for policy checks."""
144
def check(rule, target, creds, exc=None, *args, **kwargs):
146
Checks authorization of a rule against the target and credentials.
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
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
166
# Allow the rule to be a Check tree
167
if isinstance(rule, BaseCheck):
168
result = rule(target, creds)
170
# No rules to reference means we're going to fail closed
175
result = _rules[rule](target, creds)
177
# If the rule doesn't exist, fail closed
180
# If it is False, raise the exception if requested
181
if exc and result is False:
182
raise exc(*args, **kwargs)
187
class BaseCheck(object):
189
Abstract base class for Check classes.
192
__metaclass__ = abc.ABCMeta
197
Retrieve a string representation of the Check tree rooted at
204
def __call__(self, target, cred):
206
Perform the check. Returns False to reject the access or a
207
true value (not necessary True) to accept the access.
213
class FalseCheck(BaseCheck):
215
A policy check that always returns False (disallow).
219
"""Return a string representation of this check."""
223
def __call__(self, target, cred):
224
"""Check the policy."""
229
class TrueCheck(BaseCheck):
231
A policy check that always returns True (allow).
235
"""Return a string representation of this check."""
239
def __call__(self, target, cred):
240
"""Check the policy."""
245
class Check(BaseCheck):
247
A base class to allow for user-defined policy checks.
250
def __init__(self, kind, match):
252
:param kind: The kind of the check, i.e., the field before the
254
:param match: The match of the check, i.e., the field after
262
"""Return a string representation of this check."""
264
return "%s:%s" % (self.kind, self.match)
267
class NotCheck(BaseCheck):
269
A policy check that inverts the result of another policy check.
270
Implements the "not" operator.
273
def __init__(self, rule):
275
Initialize the 'not' check.
277
:param rule: The rule to negate. Must be a Check.
283
"""Return a string representation of this check."""
285
return "not %s" % self.rule
287
def __call__(self, target, cred):
289
Check the policy. Returns the logical inverse of the wrapped
293
return not self.rule(target, cred)
296
class AndCheck(BaseCheck):
298
A policy check that requires that a list of other checks all
299
return True. Implements the "and" operator.
302
def __init__(self, rules):
304
Initialize the 'and' check.
306
:param rules: A list of rules that will be tested.
312
"""Return a string representation of this check."""
314
return "(%s)" % ' and '.join(str(r) for r in self.rules)
316
def __call__(self, target, cred):
318
Check the policy. Requires that all rules accept in order to
322
for rule in self.rules:
323
if not rule(target, cred):
328
def add_check(self, rule):
330
Allows addition of another rule to the list of rules that will
331
be tested. Returns the AndCheck object for convenience.
334
self.rules.append(rule)
338
class OrCheck(BaseCheck):
340
A policy check that requires that at least one of a list of other
341
checks returns True. Implements the "or" operator.
344
def __init__(self, rules):
346
Initialize the 'or' check.
348
:param rules: A list of rules that will be tested.
354
"""Return a string representation of this check."""
356
return "(%s)" % ' or '.join(str(r) for r in self.rules)
358
def __call__(self, target, cred):
360
Check the policy. Requires that at least one rule accept in
361
order to return True.
364
for rule in self.rules:
365
if rule(target, cred):
370
def add_check(self, rule):
372
Allows addition of another rule to the list of rules that will
373
be tested. Returns the OrCheck object for convenience.
376
self.rules.append(rule)
380
def _parse_check(rule):
382
Parse a single base check rule into an appropriate Check object.
385
# Handle the special checks
392
kind, match = rule.split(':', 1)
394
LOG.exception(_("Failed to understand rule %(rule)s") % locals())
395
# If the rule is invalid, we'll fail closed
398
# Find what implements the check
400
return _checks[kind](kind, match)
401
elif None in _checks:
402
return _checks[None](kind, match)
404
LOG.error(_("No handler for matches of kind %s") % kind)
408
def _parse_list_rule(rule):
410
Provided for backwards compatibility. Translates the old
411
list-of-lists syntax into a tree of Check objects.
414
# Empty rule defaults to True
418
# Outer list is joined by "or"; inner list by "and"
420
for inner_rule in rule:
421
# Elide empty inner lists
425
# Handle bare strings
426
if isinstance(inner_rule, basestring):
427
inner_rule = [inner_rule]
429
# Parse the inner rules into Check objects
430
and_list = [_parse_check(r) for r in inner_rule]
432
# Append the appropriate check to the or_list
433
if len(and_list) == 1:
434
or_list.append(and_list[0])
436
or_list.append(AndCheck(and_list))
438
# If we have only one check, omit the "or"
439
if len(or_list) == 0:
441
elif len(or_list) == 1:
444
return OrCheck(or_list)
447
# Used for tokenizing the policy language
448
_tokenize_re = re.compile(r'\s+')
451
def _parse_tokenize(rule):
453
Tokenizer for the policy language.
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).
462
for tok in _tokenize_re.split(rule):
464
if not tok or tok.isspace():
467
# Handle leading parens on the token
468
clean = tok.lstrip('(')
469
for i in range(len(tok) - len(clean)):
472
# If it was only parentheses, continue
478
# Handle trailing parens on the token
479
clean = tok.rstrip(')')
480
trail = len(tok) - len(clean)
482
# Yield the cleaned token
483
lowered = clean.lower()
484
if lowered in ('and', 'or', 'not'):
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]
494
yield 'check', _parse_check(clean)
496
# Yield the trailing parens
497
for i in range(trail):
501
class ParseStateMeta(type):
503
Metaclass for the ParseState class. Facilitates identifying
507
def __new__(mcs, name, bases, cls_dict):
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.
516
for key, value in cls_dict.items():
517
if not hasattr(value, 'reducers'):
519
for reduction in value.reducers:
520
reducers.append((reduction, key))
522
cls_dict['reducers'] = reducers
524
return super(ParseStateMeta, mcs).__new__(mcs, name, bases, cls_dict)
527
def reducer(*tokens):
529
Decorator for reduction methods. Arguments are a sequence of
530
tokens, in order, which should trigger running this reduction
535
# Make sure we have a list of reducer sequences
536
if not hasattr(func, 'reducers'):
539
# Add the tokens to the list of reducer sequences
540
func.reducers.append(list(tokens))
547
class ParseState(object):
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.
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.
559
__metaclass__ = ParseStateMeta
562
"""Initialize the ParseState."""
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
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)
581
# Reduce the token stream
582
results = meth(*self.values[-len(reduction):])
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]
588
# Check for any more reductions
591
def shift(self, tok, value):
592
"""Adds one more token to the state. Calls reduce()."""
594
self.tokens.append(tok)
595
self.values.append(value)
597
# Do a greedy reduce...
603
Obtain the final result of the parse. Raises ValueError if
604
the parse failed to reduce to a single result.
607
if len(self.values) != 1:
608
raise ValueError("Could not parse rule")
609
return self.values[0]
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."""
617
return [('check', check)]
619
@reducer('check', 'and', 'check')
620
def _make_and_expr(self, check1, _and, check2):
622
Create an 'and_expr' from two checks joined by the 'and'
626
return [('and_expr', AndCheck([check1, check2]))]
628
@reducer('and_expr', 'and', 'check')
629
def _extend_and_expr(self, and_expr, _and, check):
631
Extend an 'and_expr' by adding one more check.
634
return [('and_expr', and_expr.add_check(check))]
636
@reducer('check', 'or', 'check')
637
def _make_or_expr(self, check1, _or, check2):
639
Create an 'or_expr' from two checks joined by the 'or'
643
return [('or_expr', OrCheck([check1, check2]))]
645
@reducer('or_expr', 'or', 'check')
646
def _extend_or_expr(self, or_expr, _or, check):
648
Extend an 'or_expr' by adding one more check.
651
return [('or_expr', or_expr.add_check(check))]
653
@reducer('not', 'check')
654
def _make_not_expr(self, _not, check):
655
"""Invert the result of another check."""
657
return [('check', NotCheck(check))]
660
def _parse_text_rule(rule):
662
Translates a policy written in the policy language into a tree of
666
# Empty rule means always accept
670
# Parse the token stream
672
for tok, value in _parse_tokenize(rule):
673
state.shift(tok, value)
678
# Couldn't parse the rule
679
LOG.exception(_("Failed to understand rule %(rule)r") % locals())
685
def parse_rule(rule):
687
Parses a policy rule into a tree of Check objects.
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)
696
def register(name, func=None):
698
Register a function or Check class as a policy check.
700
:param name: Gives the name of the check type, e.g., 'rule',
701
'role', etc. If name is None, a default check type
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.
709
# Perform the actual decoration by registering the function or
710
# class. Returns the function or class for compliance with the
711
# decorator interface.
716
# If the function or class is given, do the registration
718
return decorator(func)
724
class RuleCheck(Check):
725
def __call__(self, target, creds):
727
Recursively checks credentials based on the defined rules.
731
return _rules[self.match](target, creds)
733
# We don't have any matching rule; fail closed
738
class RoleCheck(Check):
739
def __call__(self, target, creds):
740
"""Check that there is a matching role in the cred dict."""
742
return self.match.lower() in [x.lower() for x in creds['roles']]
746
class HttpCheck(Check):
747
def __call__(self, target, creds):
749
Check http: rules by calling to a remote server.
751
This example implementation simply verifies that the response
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"
764
class GenericCheck(Check):
765
def __call__(self, target, creds):
767
Check an individual match.
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])