28
70
LOG = logging.getLogger(__name__)
35
"""Set the brain used by enforce().
37
Defaults use Brain() if not set.
45
"""Clear the brain used by enforce()."""
50
def enforce(match_list, target_dict, credentials_dict, exc=None,
52
"""Enforces authorization of some rules against credentials.
54
:param match_list: nested tuples of data to match against
56
The basic brain supports three types of match lists:
60
looks like: ``('rule:compute:get_instance',)``
62
Retrieves the named rule from the rules dict and recursively
63
checks against the contents of the rule.
67
looks like: ``('role:compute:admin',)``
69
Matches if the specified role is in credentials_dict['roles'].
73
looks like: ``('tenant_id:%(tenant_id)s',)``
75
Substitutes values from the target dict into the match using
76
the % operator and matches them against the creds dict.
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
93
('tenant_id:%(tenant_id)s', 'role:compute_sysadmin')
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
102
:param target_dict: dict of object properties
104
Target dicts contain as much information as we can about the object being
107
:param credentials_dict: dict of actor properties
109
Credentials dicts contain as much information as we can about the user
110
performing the action.
112
:param exc: exception to raise
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
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
125
if not _BRAIN.check(match_list, target_dict, credentials_dict):
127
raise exc(*args, **kwargs)
133
"""Implements policy checking."""
138
def _register(cls, name, func):
139
cls._checks[name] = func
79
A store for rules. Handles the default_rule setting directly.
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)
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)
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."""
153
self.rules = rules or {}
97
super(Rules, self).__init__(rules or {})
154
98
self.default_rule = default_rule
156
def add_rule(self, key, match):
157
self.rules[key] = match
159
def _check(self, match, target_dict, cred_dict):
161
match_kind, match_value = match.split(':', 1)
163
LOG.exception(_("Failed to understand rule %(match)r") % locals())
164
# If the rule is invalid, fail closed
169
old_func = getattr(self, '_check_%s' % match_kind)
170
except AttributeError:
171
func = self._checks.get(match_kind, self._checks.get(None, None))
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))
179
LOG.error(_("No handler for matches of kind %s") % match_kind)
183
return func(self, match_kind, match_value, target_dict, cred_dict)
185
def check(self, match_list, target_dict, cred_dict):
186
"""Checks authorization of some rules against credentials.
188
Detailed description of the check with examples in policy.enforce().
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
194
:returns: True if the check passes
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."""
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):
208
class HttpBrain(Brain):
209
"""A brain that can check external urls for policy.
211
Posts json blobs for target and credentials.
213
Note that this brain is deprecated; the http check is registered
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)
220
696
def register(name, func=None):
222
Register a function as a policy check.
698
Register a function or Check class as a policy check.
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
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.
233
# Perform the actual decoration by registering the function.
234
# Returns the function for compliance with the 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.
236
712
def decorator(func):
237
# Register the function
238
Brain._register(name, func)
241
# If the function is given, do the registration
716
# If the function or class is given, do the registration
243
718
return decorator(func)