summaryrefslogtreecommitdiffstats
path: root/openstack/common
diff options
context:
space:
mode:
authorKevin L. Mitchell <kevin.mitchell@rackspace.com>2012-10-09 13:23:26 +0100
committerMark McLoughlin <markmc@redhat.com>2012-10-09 21:16:03 +0100
commit451dd68b2d9194f4612074412acedfad800e799c (patch)
treeccba7e8b07f574fe8a6ebe9ec1168e2a4b198914 /openstack/common
parent355ca27471218012a7604b751b1ffbbd2d001f05 (diff)
downloadoslo-451dd68b2d9194f4612074412acedfad800e799c.tar.gz
oslo-451dd68b2d9194f4612074412acedfad800e799c.tar.xz
oslo-451dd68b2d9194f4612074412acedfad800e799c.zip
Rewrite the policy engine from scratch
Implements blueprint fine-grained-policy Complete rewrite of the policy engine, with careful thought given to backwards-compatibility. Policy rules are now represented internally by a tree of Check objects. A new API is added for parsing and enforcing rules: set_rules(Rules.load_json(data, default_rule=...)) result = check(rule, target, credentials) reset() The old Brain/enforce API is deprecated and will be removed soon. A new API is also added for registering new check types: @register("key") class KeyCheck(Check): def __call__(self, target, creds): return self.match in creds Support for using functions as check types is deprecated and will also be removed soon. Change-Id: I2951a0de3751bd2ec868e7a661070fed624e4af2
Diffstat (limited to 'openstack/common')
-rw-r--r--openstack/common/policy.py682
1 files changed, 502 insertions, 180 deletions
diff --git a/openstack/common/policy.py b/openstack/common/policy.py
index a4064cf..4e32a46 100644
--- a/openstack/common/policy.py
+++ b/openstack/common/policy.py
@@ -1,6 +1,6 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
-# Copyright (c) 2011 OpenStack, LLC.
+# Copyright (c) 2012 OpenStack, LLC.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
@@ -15,10 +15,24 @@
# License for the specific language governing permissions and limitations
# under the License.
-"""Common Policy Engine Implementation"""
+"""
+Common Policy Engine Implementation
+Policies are be expressed as a list-of-lists where each check inside the
+innermost list is combined as with an "and" conjunction--for that check to
+pass, all the specified checks must pass. These innermost lists are then
+combined as with an "or" conjunction.
+
+As an example, take the following rule, expressed in the list-of-lists
+representation::
+
+ [["role:admin"], ["project_id:%(project_id)s", "role:projectadmin"]]
+"""
+
+import abc
import logging
import urllib
+
import urllib2
from openstack.common.gettextutils import _
@@ -28,218 +42,522 @@ from openstack.common import jsonutils
LOG = logging.getLogger(__name__)
-_BRAIN = None
+_rules = None
+_functions = {}
+_checks = {}
-def set_brain(brain):
- """Set the brain used by enforce().
+class Rules(dict):
+ """
+ A store for rules. Handles the default_rule setting directly.
+ """
+
+ @classmethod
+ def load_json(cls, data, default_rule=None):
+ """
+ Allow loading of JSON rule data.
+ """
- Defaults use Brain() if not set.
+ # Suck in the JSON data and parse the rules
+ rules = dict((k, parse_rule(v)) for k, v in
+ jsonutils.loads(data).items())
- """
- global _BRAIN
- _BRAIN = brain
+ return cls(rules, default_rule)
+
+ def __init__(self, rules=None, default_rule=None):
+ """Initialize the Rules store."""
+
+ super(Rules, self).__init__(rules or {})
+ self.default_rule = default_rule
+
+ def __missing__(self, key):
+ """Implements the default rule handling."""
+
+ # If the default rule isn't actually defined, do something
+ # reasonably intelligent
+ if not self.default_rule or self.default_rule not in self:
+ raise KeyError(key)
+
+ return self[self.default_rule]
+
+ def __str__(self):
+ """Dumps a string representation of the rules."""
+
+ # Start by building the canonical strings for the rules
+ out_rules = {}
+ for key, value in self.items():
+ # Use empty string for singleton TrueCheck instances
+ if isinstance(value, TrueCheck):
+ out_rules[key] = ''
+ else:
+ out_rules[key] = str(value)
+ # Dump a pretty-printed JSON representation
+ return jsonutils.dumps(out_rules, indent=4)
+
+# Really have to figure out a way to deprecate this
+def set_rules(rules):
+ """Set the rules in use for policy checks."""
+
+ global _rules
+
+ _rules = rules
+
+
+# Ditto
def reset():
- """Clear the brain used by enforce()."""
- global _BRAIN
- _BRAIN = None
+ """Clear the rules used for policy checks."""
+ global _rules
-def enforce(match_list, target_dict, credentials_dict, exc=None,
- *args, **kwargs):
- """Enforces authorization of some rules against credentials.
+ _rules = None
- :param match_list: nested tuples of data to match against
- The basic brain supports three types of match lists:
+def check(rule, target, creds, exc=None, *args, **kwargs):
+ """
+ Checks authorization of a rule against the target and credentials.
+
+ :param rule: The rule to evaluate.
+ :param target: As much information about the object being operated
+ on as possible, as a dictionary.
+ :param creds: As much information about the user performing the
+ action as possible, as a dictionary.
+ :param exc: Class of the exception to raise if the check fails.
+ Any remaining arguments passed to check() (both
+ positional and keyword arguments) will be passed to
+ the exception class. If exc is not provided, returns
+ False.
+
+ :return: Returns False if the policy does not allow the action and
+ exc is not provided; otherwise, returns a value that
+ evaluates to True. Note: for rules using the "case"
+ expression, this True value will be the specified string
+ from the expression.
+ """
- 1) rules
+ # Allow the rule to be a Check tree
+ if isinstance(rule, BaseCheck):
+ result = rule(target, creds)
+ elif not _rules:
+ # No rules to reference means we're going to fail closed
+ result = False
+ else:
+ try:
+ # Evaluate the rule
+ result = _rules[rule](target, creds)
+ except KeyError:
+ # If the rule doesn't exist, fail closed
+ result = False
- looks like: ``('rule:compute:get_instance',)``
+ # If it is False, raise the exception if requested
+ if exc and result is False:
+ raise exc(*args, **kwargs)
- Retrieves the named rule from the rules dict and recursively
- checks against the contents of the rule.
+ return result
- 2) roles
- looks like: ``('role:compute:admin',)``
+class Brain(Rules):
+ """
+ Provided for backwards compatibility. Implements the functions of
+ a classic Brain. Deprecated; use Rules instead.
+ """
- Matches if the specified role is in credentials_dict['roles'].
+ def __init__(self, rules=None, default_rule=None):
+ """
+ Initialize the Brain. Logs a warning about the deprecation.
+ """
- 3) generic
+ module = self.__class__.__module__
+ name = self.__class__.__name__
+ LOG.warning(_("%(module)s.%(name)s is deprecated. "
+ "Use %(module)s.Rules instead.") % locals())
- looks like: ``('tenant_id:%(tenant_id)s',)``
+ if not rules:
+ rules = {}
- Substitutes values from the target dict into the match using
- the % operator and matches them against the creds dict.
+ # May need to transform the rules
+ new_rules = {}
+ for key, rule in rules.items():
+ if not isinstance(rule, BaseCheck):
+ # Turn it into a Check tree
+ rule = parse_rule(rule)
+ new_rules[key] = rule
- Combining rules:
+ super(Brain, self).__init__(new_rules, default_rule)
- The brain returns True if any of the outer tuple of rules
- match and also True if all of the inner tuples match. You
- can use this to perform simple boolean logic. For
- example, the following rule would return True if the creds
- contain the role 'admin' OR the if the tenant_id matches
- the target dict AND the the creds contains the role
- 'compute_sysadmin':
+ def add_rule(self, key, match):
+ """
+ Adds a rule to the Brain. Logs a warning about the
+ deprecation. Callers should use dictionary access syntax
+ instead.
+ """
- ::
+ module = self.__class__.__module__
+ name = self.__class__.__name__
+ LOG.warning(_("%(module)s.%(name)s.add_rule() is deprecated. "
+ "Use dictionary set syntax instead.") % locals())
- {
- "rule:combined": (
- 'role:admin',
- ('tenant_id:%(tenant_id)s', 'role:compute_sysadmin')
- )
- }
+ # Turn the rule into a Check tree
+ self[key] = parse_rule(match)
- Note that rule and role are reserved words in the credentials match, so
- you can't match against properties with those names. Custom brains may
- also add new reserved words. For example, the HttpBrain adds http as a
- reserved word.
+ def check(self, match_list, target, creds):
+ """
+ Performs a check. Logs a warning about the deprecation.
+ Callers should use the check() function instead.
+ """
- :param target_dict: dict of object properties
+ module = self.__class__.__module__
+ name = self.__class__.__name__
+ LOG.warning(_("%(module)s.%(name)s.check() is deprecated. "
+ "Use %(module)s.check() instead.") % locals())
- Target dicts contain as much information as we can about the object being
- operated on.
+ # Turn the rule into a check tree and evaluate it
+ check = parse_rule(match_list)
+ return check(target, creds)
- :param credentials_dict: dict of actor properties
+ @property
+ def rules(self):
+ """
+ Returns the dictionary of rules. Logs a warning about the
+ deprecation. Callers should use dictionary access instead.
+ """
- Credentials dicts contain as much information as we can about the user
- performing the action.
+ module = self.__class__.__module__
+ name = self.__class__.__name__
+ LOG.warning(_("%(module)s.%(name)s.rules is deprecated. "
+ "Use dictionary access instead.") % locals())
- :param exc: exception to raise
+ return self
- Class of the exception to raise if the check fails. Any remaining
- arguments passed to enforce() (both positional and keyword arguments)
- will be passed to the exception class. If exc is not provided, returns
- False.
- :return: True if the policy allows the action
- :return: False if the policy does not allow the action and exc is not set
+def set_brain(brain):
"""
- global _BRAIN
- if not _BRAIN:
- _BRAIN = Brain()
- if not _BRAIN.check(match_list, target_dict, credentials_dict):
- if exc:
- raise exc(*args, **kwargs)
+ Set the rules to use for policy checks. Deprecated; use the
+ set_rules() function instead. Logs a warning about the
+ deprecation.
+ """
+
+ module = __name__
+ LOG.warning(_("%(module)s.set_brain() is deprecated. "
+ "Use %(module)s.set_rules() instead.") % locals())
+
+ # Brain descends from Rules
+ return set_rules(brain)
+
+
+def enforce(match_list, target, creds, exc=None, *args, **kwargs):
+ """
+ Enforces authorization of some rules against credentials.
+ Deprecated; use the check() function instead. Logs a warning
+ about the deprecation.
+ """
+
+ module = __name__
+ LOG.warning(_("%(module)s.enforce() is deprecated. "
+ "Use %(module)s.check() instead.") % locals())
+
+ # Compile the match_list and evaluate it
+ rule = parse_rule(match_list)
+ return check(rule, target, creds, exc, *args, **kwargs)
+
+
+class BaseCheck(object):
+ """
+ Abstract base class for Check classes.
+ """
+
+ __metaclass__ = abc.ABCMeta
+
+ @abc.abstractmethod
+ def __str__(self):
+ """
+ Retrieve a string representation of the Check tree rooted at
+ this node.
+ """
+
+ pass
+
+ @abc.abstractmethod
+ def __call__(self, target, cred):
+ """
+ Perform the check. Returns False to reject the access or a
+ true value (not necessary True) to accept the access.
+ """
+
+ pass
+
+
+class FalseCheck(BaseCheck):
+ """
+ A policy check that always returns False (disallow).
+ """
+
+ def __str__(self):
+ """Return a string representation of this check."""
+
+ return "!"
+
+ def __call__(self, target, cred):
+ """Check the policy."""
+
return False
- return True
-class Brain(object):
- """Implements policy checking."""
+class TrueCheck(BaseCheck):
+ """
+ A policy check that always returns True (allow).
+ """
- _checks = {}
+ def __str__(self):
+ """Return a string representation of this check."""
- @classmethod
- def _register(cls, name, func):
- cls._checks[name] = func
+ return "@"
- @classmethod
- def load_json(cls, data, default_rule=None):
- """Init a brain using json instead of a rules dictionary."""
- rules_dict = jsonutils.loads(data)
- return cls(rules=rules_dict, default_rule=default_rule)
+ def __call__(self, target, cred):
+ """Check the policy."""
- def __init__(self, rules=None, default_rule=None):
- if self.__class__ != Brain:
- LOG.warning(_("Inheritance-based rules are deprecated; use "
- "the default brain instead of %s.") %
- self.__class__.__name__)
+ return True
- self.rules = rules or {}
- self.default_rule = default_rule
- def add_rule(self, key, match):
- self.rules[key] = match
+class Check(BaseCheck):
+ """
+ A base class to allow for user-defined policy checks.
+ """
- def _check(self, match, target_dict, cred_dict):
- try:
- match_kind, match_value = match.split(':', 1)
- except Exception:
- LOG.exception(_("Failed to understand rule %(match)r") % locals())
- # If the rule is invalid, fail closed
- return False
+ def __init__(self, kind, match):
+ """
+ :param kind: The kind of the check, i.e., the field before the
+ ':'.
+ :param match: The match of the check, i.e., the field after
+ the ':'.
+ """
- func = None
- try:
- old_func = getattr(self, '_check_%s' % match_kind)
- except AttributeError:
- func = self._checks.get(match_kind, self._checks.get(None, None))
- else:
- LOG.warning(_("Inheritance-based rules are deprecated; update "
- "_check_%s") % match_kind)
- func = lambda brain, kind, value, target, cred: old_func(value,
- target,
- cred)
-
- if not func:
- LOG.error(_("No handler for matches of kind %s") % match_kind)
- # Fail closed
- return False
+ self.kind = kind
+ self.match = match
+
+ def __str__(self):
+ """Return a string representation of this check."""
+
+ return "%s:%s" % (self.kind, self.match)
+
+
+class FuncCheck(Check):
+ """
+ A policy check that calls a function.
+ """
+
+ def __init__(self, func, kind, match):
+ """
+ Initialize the FuncCheck. Used for wrapping policy checking
+ functions.
+
+ :param func: The function that implements the actual policy
+ check.
+ :param kind: The kind of the check, i.e., the field before the
+ ':'.
+ :param match: The match of the check, i.e., the field after
+ the ':'.
+ """
+
+ super(FuncCheck, self).__init__(kind, match)
+ self.func = func
+
+ def __call__(self, target, cred):
+ """
+ Check the policy. Calls the function with a deprecated
+ leading argument.
+ """
+
+ # None argument for the deprecated "brain" argument
+ return self.func(None, self.kind, self.match, target, cred)
+
+
+class AndCheck(BaseCheck):
+ """
+ A policy check that requires that a list of other checks all
+ return True. Implements the "and" operator.
+ """
+
+ def __init__(self, rules):
+ """
+ Initialize the 'and' check.
+
+ :param rules: A list of rules that will be tested.
+ """
+
+ self.rules = rules
+
+ def __str__(self):
+ """Return a string representation of this check."""
+
+ return "(%s)" % ' and '.join(str(r) for r in self.rules)
+
+ def __call__(self, target, cred):
+ """
+ Check the policy. Requires that all rules accept in order to
+ return True.
+ """
+
+ for rule in self.rules:
+ if not rule(target, cred):
+ return False
+
+ return True
+
+ def add_check(self, rule):
+ """
+ Allows addition of another rule to the list of rules that will
+ be tested. Returns the AndCheck object for convenience.
+ """
+
+ self.rules.append(rule)
+ return self
+
+
+class OrCheck(BaseCheck):
+ """
+ A policy check that requires that at least one of a list of other
+ checks returns True. Implements the "or" operator.
+ """
- return func(self, match_kind, match_value, target_dict, cred_dict)
+ def __init__(self, rules):
+ """
+ Initialize the 'or' check.
- def check(self, match_list, target_dict, cred_dict):
- """Checks authorization of some rules against credentials.
+ :param rules: A list of rules that will be tested.
+ """
- Detailed description of the check with examples in policy.enforce().
+ self.rules = rules
- :param match_list: nested tuples of data to match against
- :param target_dict: dict of object properties
- :param credentials_dict: dict of actor properties
+ def __str__(self):
+ """Return a string representation of this check."""
- :returns: True if the check passes
+ return "(%s)" % ' or '.join(str(r) for r in self.rules)
+ def __call__(self, target, cred):
"""
- if not match_list:
- return True
- for and_list in match_list:
- if isinstance(and_list, basestring):
- and_list = (and_list,)
- if all([self._check(item, target_dict, cred_dict)
- for item in and_list]):
+ Check the policy. Requires that at least one rule accept in
+ order to return True.
+ """
+
+ for rule in self.rules:
+ if rule(target, cred):
return True
+
return False
+ def add_check(self, rule):
+ """
+ Allows addition of another rule to the list of rules that will
+ be tested. Returns the OrCheck object for convenience.
+ """
-class HttpBrain(Brain):
- """A brain that can check external urls for policy.
+ self.rules.append(rule)
+ return self
- Posts json blobs for target and credentials.
- Note that this brain is deprecated; the http check is registered
- by default.
+def _parse_check(rule):
+ """
+ Parse a single base check rule into an appropriate Check object.
+ """
+ try:
+ kind, match = rule.split(':', 1)
+ except Exception:
+ LOG.exception(_("Failed to understand rule %(rule)s") % locals())
+ # If the rule is invalid, we'll fail closed
+ return FalseCheck()
+
+ # Find what implements the check
+ if kind in _checks:
+ return _checks[kind](kind, match)
+ elif kind in _functions:
+ return FuncCheck(_functions[kind], kind, match)
+ elif None in _checks:
+ return _checks[None](kind, match)
+ elif None in _functions:
+ return FuncCheck(_functions[None], kind, match)
+ else:
+ LOG.error(_("No handler for matches of kind %s") % kind)
+ return FalseCheck()
+
+
+def _parse_list_rule(rule):
"""
+ Provided for backwards compatibility. Translates the old
+ list-of-lists syntax into a tree of Check objects.
+ """
+
+ # Empty rule defaults to True
+ if not rule:
+ return TrueCheck()
+
+ # Outer list is joined by "or"; inner list by "and"
+ or_list = []
+ for inner_rule in rule:
+ # Elide empty inner lists
+ if not inner_rule:
+ continue
+
+ # Handle bare strings
+ if isinstance(inner_rule, basestring):
+ inner_rule = [inner_rule]
+
+ # Parse the inner rules into Check objects
+ and_list = [_parse_check(r) for r in inner_rule]
+
+ # Append the appropriate check to the or_list
+ if len(and_list) == 1:
+ or_list.append(and_list[0])
+ else:
+ or_list.append(AndCheck(and_list))
+
+ # If we have only one check, omit the "or"
+ if len(or_list) == 0:
+ return FalseCheck()
+ elif len(or_list) == 1:
+ return or_list[0]
+
+ return OrCheck(or_list)
+
- pass
+def parse_rule(rule):
+ """
+ Parses a policy rule into a tree of Check objects.
+ """
+ return _parse_list_rule(rule)
def register(name, func=None):
"""
- Register a function as a policy check.
+ Register a function or Check class as a policy check.
:param name: Gives the name of the check type, e.g., 'rule',
- 'role', etc. If name is None, a default function
+ 'role', etc. If name is None, a default check type
will be registered.
- :param func: If given, provides the function to register. If not
- given, returns a function taking one argument to
- specify the function to register, allowing use as a
- decorator.
+ :param func: If given, provides the function or class to register.
+ If not given, returns a function taking one argument
+ to specify the function or class to register,
+ allowing use as a decorator.
"""
- # Perform the actual decoration by registering the function.
- # Returns the function for compliance with the decorator
- # interface.
+ # Perform the actual decoration by registering the function or
+ # class. Returns the function or class for compliance with the
+ # decorator interface.
def decorator(func):
- # Register the function
- Brain._register(name, func)
+ global _functions
+ global _checks
+
+ if issubclass(func, Check):
+ # Register the check
+ _checks[name] = func
+ else:
+ # Register the function
+ _functions[name] = func
+
return func
- # If the function is given, do the registration
+ # If the function or class is given, do the registration
if func:
return decorator(func)
@@ -247,55 +565,59 @@ def register(name, func=None):
@register("rule")
-def _check_rule(brain, match_kind, match, target_dict, cred_dict):
- """Recursively checks credentials based on the brains rules."""
- try:
- new_match_list = brain.rules[match]
- except KeyError:
- if brain.default_rule and match != brain.default_rule:
- new_match_list = ('rule:%s' % brain.default_rule,)
- else:
- return False
+class RuleCheck(Check):
+ def __call__(self, target, creds):
+ """
+ Recursively checks credentials based on the defined rules.
+ """
- return brain.check(new_match_list, target_dict, cred_dict)
+ try:
+ return _rules[self.match](target, creds)
+ except KeyError:
+ # We don't have any matching rule; fail closed
+ return False
@register("role")
-def _check_role(brain, match_kind, match, target_dict, cred_dict):
- """Check that there is a matching role in the cred dict."""
- return match.lower() in [x.lower() for x in cred_dict['roles']]
+class RoleCheck(Check):
+ def __call__(self, target, creds):
+ """Check that there is a matching role in the cred dict."""
+
+ return self.match.lower() in [x.lower() for x in creds['roles']]
@register('http')
-def _check_http(brain, match_kind, match, target_dict, cred_dict):
- """Check http: rules by calling to a remote server.
+class HttpCheck(Check):
+ def __call__(self, target, creds):
+ """
+ Check http: rules by calling to a remote server.
- This example implementation simply verifies that the response is
- exactly 'True'. A custom brain using response codes could easily
- be implemented.
+ This example implementation simply verifies that the response
+ is exactly 'True'.
+ """
- """
- url = 'http:' + (match % target_dict)
- data = {'target': jsonutils.dumps(target_dict),
- 'credentials': jsonutils.dumps(cred_dict)}
- post_data = urllib.urlencode(data)
- f = urllib2.urlopen(url, post_data)
- return f.read() == "True"
+ url = ('http:' + self.match) % target
+ data = {'target': jsonutils.dumps(target),
+ 'credentials': jsonutils.dumps(creds)}
+ post_data = urllib.urlencode(data)
+ f = urllib2.urlopen(url, post_data)
+ return f.read() == "True"
@register(None)
-def _check_generic(brain, match_kind, match, target_dict, cred_dict):
- """Check an individual match.
-
- Matches look like:
+class GenericCheck(Check):
+ def __call__(self, target, creds):
+ """
+ Check an individual match.
- tenant:%(tenant_id)s
- role:compute:admin
+ Matches look like:
- """
+ tenant:%(tenant_id)s
+ role:compute:admin
+ """
- # TODO(termie): do dict inspection via dot syntax
- match = match % target_dict
- if match_kind in cred_dict:
- return match == unicode(cred_dict[match_kind])
- return False
+ # TODO(termie): do dict inspection via dot syntax
+ match = self.match % target
+ if self.kind in creds:
+ return match == unicode(creds[self.kind])
+ return False