Update policies
Merge in update openstack-common policy code. Updates Quantum-specific policy glue code to eliminate deprecated openstack-common policy interfaces. Also cleans up policy code to allow for returning fine-grained policy values. Change-Id: I2951a0de3751bd2ec868e7a661070fed624e4af2
This commit is contained in:
parent
2e7f9eeebd
commit
a6d908b06b
@ -1,42 +1,42 @@
|
|||||||
{
|
{
|
||||||
"admin_or_owner": [["role:admin"], ["tenant_id:%(tenant_id)s"]],
|
"admin_or_owner": "role:admin or tenant_id:%(tenant_id)s",
|
||||||
"admin_or_network_owner": [["role:admin"], ["tenant_id:%(network_tenant_id)s"]],
|
"admin_or_network_owner": "role:admin or tenant_id:%(network_tenant_id)s",
|
||||||
"admin_only": [["role:admin"]],
|
"admin_only": "role:admin",
|
||||||
"regular_user": [],
|
"regular_user": "",
|
||||||
"shared": [["field:networks:shared=True"]],
|
"shared": "field:networks:shared=True",
|
||||||
"external": [["field:networks:router:external=True"]],
|
"external": "field:networks:router:external=True",
|
||||||
"default": [["rule:admin_or_owner"]],
|
"default": "rule:admin_or_owner",
|
||||||
|
|
||||||
"extension:provider_network:view": [["rule:admin_only"]],
|
"extension:provider_network:view": "rule:admin_only",
|
||||||
"extension:provider_network:set": [["rule:admin_only"]],
|
"extension:provider_network:set": "rule:admin_only",
|
||||||
|
|
||||||
"extension:router:view": [["rule:regular_user"]],
|
"extension:router:view": "rule:regular_user",
|
||||||
"extension:router:set": [["rule:admin_only"]],
|
"extension:router:set": "rule:admin_only",
|
||||||
"extension:router:add_router_interface": [["rule:admin_or_owner"]],
|
"extension:router:add_router_interface": "rule:admin_or_owner",
|
||||||
"extension:router:remove_router_interface": [["rule:admin_or_owner"]],
|
"extension:router:remove_router_interface": "rule:admin_or_owner",
|
||||||
|
|
||||||
"subnets:private:read": [["rule:admin_or_owner"]],
|
"subnets:private:read": "rule:admin_or_owner",
|
||||||
"subnets:private:write": [["rule:admin_or_owner"]],
|
"subnets:private:write": "rule:admin_or_owner",
|
||||||
"subnets:shared:read": [["rule:regular_user"]],
|
"subnets:shared:read": "rule:regular_user",
|
||||||
"subnets:shared:write": [["rule:admin_only"]],
|
"subnets:shared:write": "rule:admin_only",
|
||||||
|
|
||||||
"create_subnet": [["rule:admin_or_network_owner"]],
|
"create_subnet": "rule:admin_or_network_owner",
|
||||||
"get_subnet": [["rule:admin_or_owner"], ["rule:shared"]],
|
"get_subnet": "rule:admin_or_owner or rule:shared",
|
||||||
"update_subnet": [["rule:admin_or_network_owner"]],
|
"update_subnet": "rule:admin_or_network_owner",
|
||||||
"delete_subnet": [["rule:admin_or_network_owner"]],
|
"delete_subnet": "rule:admin_or_network_owner",
|
||||||
|
|
||||||
"create_network": [],
|
"create_network": "",
|
||||||
"get_network": [["rule:admin_or_owner"], ["rule:shared"], ["rule:external"]],
|
"get_network": "rule:admin_or_owner or rule:shared or rule:external",
|
||||||
"create_network:shared": [["rule:admin_only"]],
|
"create_network:shared": "rule:admin_only",
|
||||||
"create_network:router:external": [["rule:admin_only"]],
|
"create_network:router:external": "rule:admin_only",
|
||||||
"update_network": [["rule:admin_or_owner"]],
|
"update_network": "rule:admin_or_owner",
|
||||||
"delete_network": [["rule:admin_or_owner"]],
|
"delete_network": "rule:admin_or_owner",
|
||||||
|
|
||||||
"create_port": [],
|
"create_port": "",
|
||||||
"create_port:mac_address": [["rule:admin_or_network_owner"]],
|
"create_port:mac_address": "rule:admin_or_network_owner",
|
||||||
"create_port:fixed_ips": [["rule:admin_or_network_owner"]],
|
"create_port:fixed_ips": "rule:admin_or_network_owner",
|
||||||
"get_port": [["rule:admin_or_owner"]],
|
"get_port": "rule:admin_or_owner",
|
||||||
"update_port": [["rule:admin_or_owner"]],
|
"update_port": "rule:admin_or_owner",
|
||||||
"update_port:fixed_ips": [["rule:admin_or_network_owner"]],
|
"update_port:fixed_ips": "rule:admin_or_network_owner",
|
||||||
"delete_port": [["rule:admin_or_owner"]]
|
"delete_port": "rule:admin_or_owner"
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
# Copyright (c) 2011 OpenStack, LLC.
|
# Copyright (c) 2012 OpenStack, LLC.
|
||||||
# All Rights Reserved.
|
# All Rights Reserved.
|
||||||
#
|
#
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
@ -15,10 +15,52 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
"""Common Policy Engine Implementation"""
|
"""
|
||||||
|
Common Policy Engine Implementation
|
||||||
|
|
||||||
|
Policies can be expressed in one of two forms: A list of lists, or a
|
||||||
|
string written in the new policy language.
|
||||||
|
|
||||||
|
In the list-of-lists representation, 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. This is the original way of
|
||||||
|
expressing policies, but there now exists a new way: the policy
|
||||||
|
language.
|
||||||
|
|
||||||
|
In the policy language, each check is specified the same way as in the
|
||||||
|
list-of-lists representation: a simple "a:b" pair that is matched to
|
||||||
|
the correct code to perform that check. However, conjunction
|
||||||
|
operators are available, allowing for more expressiveness in crafting
|
||||||
|
policies.
|
||||||
|
|
||||||
|
As an example, take the following rule, expressed in the list-of-lists
|
||||||
|
representation::
|
||||||
|
|
||||||
|
[["role:admin"], ["project_id:%(project_id)s", "role:projectadmin"]]
|
||||||
|
|
||||||
|
In the policy language, this becomes::
|
||||||
|
|
||||||
|
role:admin or (project_id:%(project_id)s and role:projectadmin)
|
||||||
|
|
||||||
|
The policy language also has the "not" operator, allowing a richer
|
||||||
|
policy rule::
|
||||||
|
|
||||||
|
project_id:%(project_id)s and not role:dunce
|
||||||
|
|
||||||
|
Finally, two special policy checks should be mentioned; the policy
|
||||||
|
check "@" will always accept an access, and the policy check "!" will
|
||||||
|
always reject an access. (Note that if a rule is either the empty
|
||||||
|
list ("[]") or the empty string, this is equivalent to the "@" policy
|
||||||
|
check.) Of these, the "!" policy check is probably the most useful,
|
||||||
|
as it allows particular rules to be explicitly disabled.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import abc
|
||||||
import logging
|
import logging
|
||||||
|
import re
|
||||||
import urllib
|
import urllib
|
||||||
|
|
||||||
import urllib2
|
import urllib2
|
||||||
|
|
||||||
from quantum.openstack.common.gettextutils import _
|
from quantum.openstack.common.gettextutils import _
|
||||||
@ -28,217 +70,650 @@ from quantum.openstack.common import jsonutils
|
|||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
_BRAIN = None
|
_rules = None
|
||||||
|
_checks = {}
|
||||||
|
|
||||||
|
|
||||||
def set_brain(brain):
|
class Rules(dict):
|
||||||
"""Set the brain used by enforce().
|
|
||||||
|
|
||||||
Defaults use Brain() if not set.
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
global _BRAIN
|
A store for rules. Handles the default_rule setting directly.
|
||||||
_BRAIN = brain
|
|
||||||
|
|
||||||
|
|
||||||
def reset():
|
|
||||||
"""Clear the brain used by enforce()."""
|
|
||||||
global _BRAIN
|
|
||||||
_BRAIN = None
|
|
||||||
|
|
||||||
|
|
||||||
def enforce(match_list, target_dict, credentials_dict, exc=None,
|
|
||||||
*args, **kwargs):
|
|
||||||
"""Enforces authorization of some rules against credentials.
|
|
||||||
|
|
||||||
:param match_list: nested tuples of data to match against
|
|
||||||
|
|
||||||
The basic brain supports three types of match lists:
|
|
||||||
|
|
||||||
1) rules
|
|
||||||
|
|
||||||
looks like: ``('rule:compute:get_instance',)``
|
|
||||||
|
|
||||||
Retrieves the named rule from the rules dict and recursively
|
|
||||||
checks against the contents of the rule.
|
|
||||||
|
|
||||||
2) roles
|
|
||||||
|
|
||||||
looks like: ``('role:compute:admin',)``
|
|
||||||
|
|
||||||
Matches if the specified role is in credentials_dict['roles'].
|
|
||||||
|
|
||||||
3) generic
|
|
||||||
|
|
||||||
looks like: ``('tenant_id:%(tenant_id)s',)``
|
|
||||||
|
|
||||||
Substitutes values from the target dict into the match using
|
|
||||||
the % operator and matches them against the creds dict.
|
|
||||||
|
|
||||||
Combining rules:
|
|
||||||
|
|
||||||
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':
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
{
|
|
||||||
"rule:combined": (
|
|
||||||
'role:admin',
|
|
||||||
('tenant_id:%(tenant_id)s', 'role:compute_sysadmin')
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
:param target_dict: dict of object properties
|
|
||||||
|
|
||||||
Target dicts contain as much information as we can about the object being
|
|
||||||
operated on.
|
|
||||||
|
|
||||||
:param credentials_dict: dict of actor properties
|
|
||||||
|
|
||||||
Credentials dicts contain as much information as we can about the user
|
|
||||||
performing the action.
|
|
||||||
|
|
||||||
:param exc: exception to raise
|
|
||||||
|
|
||||||
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
|
|
||||||
"""
|
"""
|
||||||
global _BRAIN
|
|
||||||
if not _BRAIN:
|
|
||||||
_BRAIN = Brain()
|
|
||||||
if not _BRAIN.check(match_list, target_dict, credentials_dict):
|
|
||||||
if exc:
|
|
||||||
raise exc(*args, **kwargs)
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
class Brain(object):
|
|
||||||
"""Implements policy checking."""
|
|
||||||
|
|
||||||
_checks = {}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def _register(cls, name, func):
|
|
||||||
cls._checks[name] = func
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def load_json(cls, data, default_rule=None):
|
def load_json(cls, data, default_rule=None):
|
||||||
"""Init a brain using json instead of a rules dictionary."""
|
"""
|
||||||
rules_dict = jsonutils.loads(data)
|
Allow loading of JSON rule data.
|
||||||
return cls(rules=rules_dict, default_rule=default_rule)
|
"""
|
||||||
|
|
||||||
|
# Suck in the JSON data and parse the rules
|
||||||
|
rules = dict((k, parse_rule(v)) for k, v in
|
||||||
|
jsonutils.loads(data).items())
|
||||||
|
|
||||||
|
return cls(rules, default_rule)
|
||||||
|
|
||||||
def __init__(self, rules=None, default_rule=None):
|
def __init__(self, rules=None, default_rule=None):
|
||||||
if self.__class__ != Brain:
|
"""Initialize the Rules store."""
|
||||||
LOG.warning(_("Inheritance-based rules are deprecated; use "
|
|
||||||
"the default brain instead of %s.") %
|
|
||||||
self.__class__.__name__)
|
|
||||||
|
|
||||||
self.rules = rules or {}
|
super(Rules, self).__init__(rules or {})
|
||||||
self.default_rule = default_rule
|
self.default_rule = default_rule
|
||||||
|
|
||||||
def add_rule(self, key, match):
|
def __missing__(self, key):
|
||||||
self.rules[key] = match
|
"""Implements the default rule handling."""
|
||||||
|
|
||||||
def _check(self, match, target_dict, cred_dict):
|
# 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 rules used for policy checks."""
|
||||||
|
|
||||||
|
global _rules
|
||||||
|
|
||||||
|
_rules = None
|
||||||
|
|
||||||
|
|
||||||
|
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.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# 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:
|
try:
|
||||||
match_kind, match_value = match.split(':', 1)
|
# Evaluate the rule
|
||||||
except Exception:
|
result = _rules[rule](target, creds)
|
||||||
LOG.exception(_("Failed to understand rule %(match)r") % locals())
|
except KeyError:
|
||||||
# If the rule is invalid, fail closed
|
# If the rule doesn't exist, fail closed
|
||||||
return False
|
result = False
|
||||||
|
|
||||||
func = None
|
# If it is False, raise the exception if requested
|
||||||
try:
|
if exc and result is False:
|
||||||
old_func = getattr(self, '_check_%s' % match_kind)
|
raise exc(*args, **kwargs)
|
||||||
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:
|
return result
|
||||||
LOG.error(_("No handler for matches of kind %s") % match_kind)
|
|
||||||
# Fail closed
|
|
||||||
return False
|
|
||||||
|
|
||||||
return func(self, match_kind, match_value, target_dict, cred_dict)
|
|
||||||
|
|
||||||
def check(self, match_list, target_dict, cred_dict):
|
class BaseCheck(object):
|
||||||
"""Checks authorization of some rules against credentials.
|
"""
|
||||||
|
Abstract base class for Check classes.
|
||||||
|
"""
|
||||||
|
|
||||||
Detailed description of the check with examples in policy.enforce().
|
__metaclass__ = abc.ABCMeta
|
||||||
|
|
||||||
:param match_list: nested tuples of data to match against
|
|
||||||
:param target_dict: dict of object properties
|
|
||||||
:param credentials_dict: dict of actor properties
|
|
||||||
|
|
||||||
:returns: True if the check passes
|
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def __str__(self):
|
||||||
"""
|
"""
|
||||||
if not match_list:
|
Retrieve a string representation of the Check tree rooted at
|
||||||
return True
|
this node.
|
||||||
for and_list in match_list:
|
"""
|
||||||
if isinstance(and_list, basestring):
|
|
||||||
and_list = (and_list,)
|
pass
|
||||||
if all([self._check(item, target_dict, cred_dict)
|
|
||||||
for item in and_list]):
|
@abc.abstractmethod
|
||||||
return True
|
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 False
|
||||||
|
|
||||||
|
|
||||||
class HttpBrain(Brain):
|
class TrueCheck(BaseCheck):
|
||||||
"""A brain that can check external urls for policy.
|
"""
|
||||||
|
A policy check that always returns True (allow).
|
||||||
Posts json blobs for target and credentials.
|
|
||||||
|
|
||||||
Note that this brain is deprecated; the http check is registered
|
|
||||||
by default.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
pass
|
def __str__(self):
|
||||||
|
"""Return a string representation of this check."""
|
||||||
|
|
||||||
|
return "@"
|
||||||
|
|
||||||
|
def __call__(self, target, cred):
|
||||||
|
"""Check the policy."""
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class Check(BaseCheck):
|
||||||
|
"""
|
||||||
|
A base class to allow for user-defined policy checks.
|
||||||
|
"""
|
||||||
|
|
||||||
|
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 ':'.
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.kind = kind
|
||||||
|
self.match = match
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
"""Return a string representation of this check."""
|
||||||
|
|
||||||
|
return "%s:%s" % (self.kind, self.match)
|
||||||
|
|
||||||
|
|
||||||
|
class NotCheck(BaseCheck):
|
||||||
|
"""
|
||||||
|
A policy check that inverts the result of another policy check.
|
||||||
|
Implements the "not" operator.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, rule):
|
||||||
|
"""
|
||||||
|
Initialize the 'not' check.
|
||||||
|
|
||||||
|
:param rule: The rule to negate. Must be a Check.
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.rule = rule
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
"""Return a string representation of this check."""
|
||||||
|
|
||||||
|
return "not %s" % self.rule
|
||||||
|
|
||||||
|
def __call__(self, target, cred):
|
||||||
|
"""
|
||||||
|
Check the policy. Returns the logical inverse of the wrapped
|
||||||
|
check.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return not self.rule(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.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, rules):
|
||||||
|
"""
|
||||||
|
Initialize the 'or' 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)" % ' or '.join(str(r) for r in self.rules)
|
||||||
|
|
||||||
|
def __call__(self, target, cred):
|
||||||
|
"""
|
||||||
|
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.
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.rules.append(rule)
|
||||||
|
return self
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_check(rule):
|
||||||
|
"""
|
||||||
|
Parse a single base check rule into an appropriate Check object.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Handle the special checks
|
||||||
|
if rule == '!':
|
||||||
|
return FalseCheck()
|
||||||
|
elif rule == '@':
|
||||||
|
return TrueCheck()
|
||||||
|
|
||||||
|
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 None in _checks:
|
||||||
|
return _checks[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)
|
||||||
|
|
||||||
|
|
||||||
|
# Used for tokenizing the policy language
|
||||||
|
_tokenize_re = re.compile(r'\s+')
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_tokenize(rule):
|
||||||
|
"""
|
||||||
|
Tokenizer for the policy language.
|
||||||
|
|
||||||
|
Most of the single-character tokens are specified in the
|
||||||
|
_tokenize_re; however, parentheses need to be handled specially,
|
||||||
|
because they can appear inside a check string. Thankfully, those
|
||||||
|
parentheses that appear inside a check string can never occur at
|
||||||
|
the very beginning or end ("%(variable)s" is the correct syntax).
|
||||||
|
"""
|
||||||
|
|
||||||
|
for tok in _tokenize_re.split(rule):
|
||||||
|
# Skip empty tokens
|
||||||
|
if not tok or tok.isspace():
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Handle leading parens on the token
|
||||||
|
clean = tok.lstrip('(')
|
||||||
|
for i in range(len(tok) - len(clean)):
|
||||||
|
yield '(', '('
|
||||||
|
|
||||||
|
# If it was only parentheses, continue
|
||||||
|
if not clean:
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
tok = clean
|
||||||
|
|
||||||
|
# Handle trailing parens on the token
|
||||||
|
clean = tok.rstrip(')')
|
||||||
|
trail = len(tok) - len(clean)
|
||||||
|
|
||||||
|
# Yield the cleaned token
|
||||||
|
lowered = clean.lower()
|
||||||
|
if lowered in ('and', 'or', 'not'):
|
||||||
|
# Special tokens
|
||||||
|
yield lowered, clean
|
||||||
|
elif clean:
|
||||||
|
# Not a special token, but not composed solely of ')'
|
||||||
|
if len(tok) >= 2 and ((tok[0], tok[-1]) in
|
||||||
|
[('"', '"'), ("'", "'")]):
|
||||||
|
# It's a quoted string
|
||||||
|
yield 'string', tok[1:-1]
|
||||||
|
else:
|
||||||
|
yield 'check', _parse_check(clean)
|
||||||
|
|
||||||
|
# Yield the trailing parens
|
||||||
|
for i in range(trail):
|
||||||
|
yield ')', ')'
|
||||||
|
|
||||||
|
|
||||||
|
class ParseStateMeta(type):
|
||||||
|
"""
|
||||||
|
Metaclass for the ParseState class. Facilitates identifying
|
||||||
|
reduction methods.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __new__(mcs, name, bases, cls_dict):
|
||||||
|
"""
|
||||||
|
Create the class. Injects the 'reducers' list, a list of
|
||||||
|
tuples matching token sequences to the names of the
|
||||||
|
corresponding reduction methods.
|
||||||
|
"""
|
||||||
|
|
||||||
|
reducers = []
|
||||||
|
|
||||||
|
for key, value in cls_dict.items():
|
||||||
|
if not hasattr(value, 'reducers'):
|
||||||
|
continue
|
||||||
|
for reduction in value.reducers:
|
||||||
|
reducers.append((reduction, key))
|
||||||
|
|
||||||
|
cls_dict['reducers'] = reducers
|
||||||
|
|
||||||
|
return super(ParseStateMeta, mcs).__new__(mcs, name, bases, cls_dict)
|
||||||
|
|
||||||
|
|
||||||
|
def reducer(*tokens):
|
||||||
|
"""
|
||||||
|
Decorator for reduction methods. Arguments are a sequence of
|
||||||
|
tokens, in order, which should trigger running this reduction
|
||||||
|
method.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def decorator(func):
|
||||||
|
# Make sure we have a list of reducer sequences
|
||||||
|
if not hasattr(func, 'reducers'):
|
||||||
|
func.reducers = []
|
||||||
|
|
||||||
|
# Add the tokens to the list of reducer sequences
|
||||||
|
func.reducers.append(list(tokens))
|
||||||
|
|
||||||
|
return func
|
||||||
|
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
class ParseState(object):
|
||||||
|
"""
|
||||||
|
Implement the core of parsing the policy language. Uses a greedy
|
||||||
|
reduction algorithm to reduce a sequence of tokens into a single
|
||||||
|
terminal, the value of which will be the root of the Check tree.
|
||||||
|
|
||||||
|
Note: error reporting is rather lacking. The best we can get with
|
||||||
|
this parser formulation is an overall "parse failed" error.
|
||||||
|
Fortunately, the policy language is simple enough that this
|
||||||
|
shouldn't be that big a problem.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__metaclass__ = ParseStateMeta
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
"""Initialize the ParseState."""
|
||||||
|
|
||||||
|
self.tokens = []
|
||||||
|
self.values = []
|
||||||
|
|
||||||
|
def reduce(self):
|
||||||
|
"""
|
||||||
|
Perform a greedy reduction of the token stream. If a reducer
|
||||||
|
method matches, it will be executed, then the reduce() method
|
||||||
|
will be called recursively to search for any more possible
|
||||||
|
reductions.
|
||||||
|
"""
|
||||||
|
|
||||||
|
for reduction, methname in self.reducers:
|
||||||
|
if (len(self.tokens) >= len(reduction) and
|
||||||
|
self.tokens[-len(reduction):] == reduction):
|
||||||
|
# Get the reduction method
|
||||||
|
meth = getattr(self, methname)
|
||||||
|
|
||||||
|
# Reduce the token stream
|
||||||
|
results = meth(*self.values[-len(reduction):])
|
||||||
|
|
||||||
|
# Update the tokens and values
|
||||||
|
self.tokens[-len(reduction):] = [r[0] for r in results]
|
||||||
|
self.values[-len(reduction):] = [r[1] for r in results]
|
||||||
|
|
||||||
|
# Check for any more reductions
|
||||||
|
return self.reduce()
|
||||||
|
|
||||||
|
def shift(self, tok, value):
|
||||||
|
"""Adds one more token to the state. Calls reduce()."""
|
||||||
|
|
||||||
|
self.tokens.append(tok)
|
||||||
|
self.values.append(value)
|
||||||
|
|
||||||
|
# Do a greedy reduce...
|
||||||
|
self.reduce()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def result(self):
|
||||||
|
"""
|
||||||
|
Obtain the final result of the parse. Raises ValueError if
|
||||||
|
the parse failed to reduce to a single result.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if len(self.values) != 1:
|
||||||
|
raise ValueError("Could not parse rule")
|
||||||
|
return self.values[0]
|
||||||
|
|
||||||
|
@reducer('(', 'check', ')')
|
||||||
|
@reducer('(', 'and_expr', ')')
|
||||||
|
@reducer('(', 'or_expr', ')')
|
||||||
|
def _wrap_check(self, _p1, check, _p2):
|
||||||
|
"""Turn parenthesized expressions into a 'check' token."""
|
||||||
|
|
||||||
|
return [('check', check)]
|
||||||
|
|
||||||
|
@reducer('check', 'and', 'check')
|
||||||
|
def _make_and_expr(self, check1, _and, check2):
|
||||||
|
"""
|
||||||
|
Create an 'and_expr' from two checks joined by the 'and'
|
||||||
|
operator.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return [('and_expr', AndCheck([check1, check2]))]
|
||||||
|
|
||||||
|
@reducer('and_expr', 'and', 'check')
|
||||||
|
def _extend_and_expr(self, and_expr, _and, check):
|
||||||
|
"""
|
||||||
|
Extend an 'and_expr' by adding one more check.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return [('and_expr', and_expr.add_check(check))]
|
||||||
|
|
||||||
|
@reducer('check', 'or', 'check')
|
||||||
|
def _make_or_expr(self, check1, _or, check2):
|
||||||
|
"""
|
||||||
|
Create an 'or_expr' from two checks joined by the 'or'
|
||||||
|
operator.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return [('or_expr', OrCheck([check1, check2]))]
|
||||||
|
|
||||||
|
@reducer('or_expr', 'or', 'check')
|
||||||
|
def _extend_or_expr(self, or_expr, _or, check):
|
||||||
|
"""
|
||||||
|
Extend an 'or_expr' by adding one more check.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return [('or_expr', or_expr.add_check(check))]
|
||||||
|
|
||||||
|
@reducer('not', 'check')
|
||||||
|
def _make_not_expr(self, _not, check):
|
||||||
|
"""Invert the result of another check."""
|
||||||
|
|
||||||
|
return [('check', NotCheck(check))]
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_text_rule(rule):
|
||||||
|
"""
|
||||||
|
Translates a policy written in the policy language into a tree of
|
||||||
|
Check objects.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Empty rule means always accept
|
||||||
|
if not rule:
|
||||||
|
return TrueCheck()
|
||||||
|
|
||||||
|
# Parse the token stream
|
||||||
|
state = ParseState()
|
||||||
|
for tok, value in _parse_tokenize(rule):
|
||||||
|
state.shift(tok, value)
|
||||||
|
|
||||||
|
try:
|
||||||
|
return state.result
|
||||||
|
except ValueError:
|
||||||
|
# Couldn't parse the rule
|
||||||
|
LOG.exception(_("Failed to understand rule %(rule)r") % locals())
|
||||||
|
|
||||||
|
# Fail closed
|
||||||
|
return FalseCheck()
|
||||||
|
|
||||||
|
|
||||||
|
def parse_rule(rule):
|
||||||
|
"""
|
||||||
|
Parses a policy rule into a tree of Check objects.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# If the rule is a string, it's in the policy language
|
||||||
|
if isinstance(rule, basestring):
|
||||||
|
return _parse_text_rule(rule)
|
||||||
|
return _parse_list_rule(rule)
|
||||||
|
|
||||||
|
|
||||||
def register(name, func=None):
|
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',
|
: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.
|
will be registered.
|
||||||
:param func: If given, provides the function to register. If not
|
:param func: If given, provides the function or class to register.
|
||||||
given, returns a function taking one argument to
|
If not given, returns a function taking one argument
|
||||||
specify the function to register, allowing use as a
|
to specify the function or class to register,
|
||||||
decorator.
|
allowing use as a decorator.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Perform the actual decoration by registering the function.
|
# Perform the actual decoration by registering the function or
|
||||||
# Returns the function for compliance with the decorator
|
# class. Returns the function or class for compliance with the
|
||||||
# interface.
|
# decorator interface.
|
||||||
def decorator(func):
|
def decorator(func):
|
||||||
# Register the function
|
_checks[name] = func
|
||||||
Brain._register(name, func)
|
|
||||||
return func
|
return func
|
||||||
|
|
||||||
# If the function is given, do the registration
|
# If the function or class is given, do the registration
|
||||||
if func:
|
if func:
|
||||||
return decorator(func)
|
return decorator(func)
|
||||||
|
|
||||||
@ -246,55 +721,59 @@ def register(name, func=None):
|
|||||||
|
|
||||||
|
|
||||||
@register("rule")
|
@register("rule")
|
||||||
def _check_rule(brain, match_kind, match, target_dict, cred_dict):
|
class RuleCheck(Check):
|
||||||
"""Recursively checks credentials based on the brains rules."""
|
def __call__(self, target, creds):
|
||||||
try:
|
"""
|
||||||
new_match_list = brain.rules[match]
|
Recursively checks credentials based on the defined rules.
|
||||||
except KeyError:
|
"""
|
||||||
if brain.default_rule and match != brain.default_rule:
|
|
||||||
new_match_list = ('rule:%s' % brain.default_rule,)
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
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")
|
@register("role")
|
||||||
def _check_role(brain, match_kind, match, target_dict, cred_dict):
|
class RoleCheck(Check):
|
||||||
"""Check that there is a matching role in the cred dict."""
|
def __call__(self, target, creds):
|
||||||
return match.lower() in [x.lower() for x in cred_dict['roles']]
|
"""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')
|
@register('http')
|
||||||
def _check_http(brain, match_kind, match, target_dict, cred_dict):
|
class HttpCheck(Check):
|
||||||
"""Check http: rules by calling to a remote server.
|
def __call__(self, target, creds):
|
||||||
|
"""
|
||||||
|
Check http: rules by calling to a remote server.
|
||||||
|
|
||||||
This example implementation simply verifies that the response is
|
This example implementation simply verifies that the response
|
||||||
exactly 'True'. A custom brain using response codes could easily
|
is exactly 'True'.
|
||||||
be implemented.
|
"""
|
||||||
|
|
||||||
"""
|
url = ('http:' + self.match) % target
|
||||||
url = 'http:' + (match % target_dict)
|
data = {'target': jsonutils.dumps(target),
|
||||||
data = {'target': jsonutils.dumps(target_dict),
|
'credentials': jsonutils.dumps(creds)}
|
||||||
'credentials': jsonutils.dumps(cred_dict)}
|
post_data = urllib.urlencode(data)
|
||||||
post_data = urllib.urlencode(data)
|
f = urllib2.urlopen(url, post_data)
|
||||||
f = urllib2.urlopen(url, post_data)
|
return f.read() == "True"
|
||||||
return f.read() == "True"
|
|
||||||
|
|
||||||
|
|
||||||
@register(None)
|
@register(None)
|
||||||
def _check_generic(brain, match_kind, match, target_dict, cred_dict):
|
class GenericCheck(Check):
|
||||||
"""Check an individual match.
|
def __call__(self, target, creds):
|
||||||
|
"""
|
||||||
|
Check an individual match.
|
||||||
|
|
||||||
Matches look like:
|
Matches look like:
|
||||||
|
|
||||||
tenant:%(tenant_id)s
|
tenant:%(tenant_id)s
|
||||||
role:compute:admin
|
role:compute:admin
|
||||||
|
"""
|
||||||
|
|
||||||
"""
|
# TODO(termie): do dict inspection via dot syntax
|
||||||
|
match = self.match % target
|
||||||
# TODO(termie): do dict inspection via dot syntax
|
if self.kind in creds:
|
||||||
match = match % target_dict
|
return match == unicode(creds[self.kind])
|
||||||
if match_kind in cred_dict:
|
return False
|
||||||
return match == unicode(cred_dict[match_kind])
|
|
||||||
return False
|
|
||||||
|
@ -50,7 +50,7 @@ def init():
|
|||||||
# pass _set_brain to read_cached_file so that the policy brain
|
# pass _set_brain to read_cached_file so that the policy brain
|
||||||
# is reset only if the file has changed
|
# is reset only if the file has changed
|
||||||
utils.read_cached_file(_POLICY_PATH, _POLICY_CACHE,
|
utils.read_cached_file(_POLICY_PATH, _POLICY_CACHE,
|
||||||
reload_func=_set_brain)
|
reload_func=_set_rules)
|
||||||
|
|
||||||
|
|
||||||
def get_resource_and_action(action):
|
def get_resource_and_action(action):
|
||||||
@ -59,9 +59,9 @@ def get_resource_and_action(action):
|
|||||||
return ("%ss" % data[-1], data[0] != 'get')
|
return ("%ss" % data[-1], data[0] != 'get')
|
||||||
|
|
||||||
|
|
||||||
def _set_brain(data):
|
def _set_rules(data):
|
||||||
default_rule = 'default'
|
default_rule = 'default'
|
||||||
policy.set_brain(policy.Brain.load_json(data, default_rule))
|
policy.set_rules(policy.Rules.load_json(data, default_rule))
|
||||||
|
|
||||||
|
|
||||||
def _is_attribute_explicitly_set(attribute_name, resource, target):
|
def _is_attribute_explicitly_set(attribute_name, resource, target):
|
||||||
@ -95,10 +95,10 @@ def _build_target(action, original_target, plugin, context):
|
|||||||
return target
|
return target
|
||||||
|
|
||||||
|
|
||||||
def _build_match_list(action, target):
|
def _build_match_rule(action, target):
|
||||||
"""Create the list of rules to match for a given action.
|
"""Create the rule to match for a given action.
|
||||||
|
|
||||||
The list of policy rules to be matched is built in the following way:
|
The policy rule to be matched is built in the following way:
|
||||||
1) add entries for matching permission on objects
|
1) add entries for matching permission on objects
|
||||||
2) add an entry for the specific action (e.g.: create_network)
|
2) add an entry for the specific action (e.g.: create_network)
|
||||||
3) add an entry for attributes of a resource for which the action
|
3) add an entry for attributes of a resource for which the action
|
||||||
@ -106,7 +106,7 @@ def _build_match_list(action, target):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
match_list = ('rule:%s' % action,)
|
match_rule = policy.RuleCheck('rule', action)
|
||||||
resource, is_write = get_resource_and_action(action)
|
resource, is_write = get_resource_and_action(action)
|
||||||
if is_write:
|
if is_write:
|
||||||
# assigning to variable with short name for improving readability
|
# assigning to variable with short name for improving readability
|
||||||
@ -118,37 +118,42 @@ def _build_match_list(action, target):
|
|||||||
target):
|
target):
|
||||||
attribute = res_map[resource][attribute_name]
|
attribute = res_map[resource][attribute_name]
|
||||||
if 'enforce_policy' in attribute and is_write:
|
if 'enforce_policy' in attribute and is_write:
|
||||||
match_list += ('rule:%s:%s' % (action,
|
attr_rule = policy.RuleCheck('rule', '%s:%s' %
|
||||||
attribute_name),)
|
(action, attribute_name))
|
||||||
return [match_list]
|
match_rule = policy.AndCheck([match_rule, attr_rule])
|
||||||
|
|
||||||
|
return match_rule
|
||||||
|
|
||||||
|
|
||||||
@policy.register('field')
|
@policy.register('field')
|
||||||
def check_field(brain, match_kind, match, target_dict, cred_dict):
|
class FieldCheck(policy.Check):
|
||||||
# If this method is invoked for the wrong kind of match
|
def __init__(self, kind, match):
|
||||||
# which should never happen, just skip the check and don't
|
# Process the match
|
||||||
# fail the policy evaluation
|
resource, field_value = match.split(':', 1)
|
||||||
if match_kind != 'field':
|
field, value = field_value.split('=', 1)
|
||||||
LOG.warning("Field check function invoked with wrong match_kind:%s",
|
|
||||||
match_kind)
|
super(FieldCheck, self).__init__(kind, '%s:%s:%s' %
|
||||||
return True
|
(resource, field, value))
|
||||||
resource, field_value = match.split(':', 1)
|
|
||||||
field, value = field_value.split('=', 1)
|
# Value might need conversion - we need help from the attribute map
|
||||||
target_value = target_dict.get(field)
|
try:
|
||||||
# target_value might be a boolean, explicitly compare with None
|
attr = attributes.RESOURCE_ATTRIBUTE_MAP[resource][field]
|
||||||
if target_value is None:
|
conv_func = attr['convert_to']
|
||||||
LOG.debug("Unable to find requested field: %s in target: %s",
|
except KeyError:
|
||||||
field, target_dict)
|
conv_func = lambda x: x
|
||||||
return False
|
|
||||||
# Value migth need conversion - we need help from the attribute map
|
self.field = field
|
||||||
conv_func = attributes.RESOURCE_ATTRIBUTE_MAP[resource][field].get(
|
self.value = conv_func(value)
|
||||||
'convert_to', lambda x: x)
|
|
||||||
if target_value != conv_func(value):
|
def __call__(self, target_dict, cred_dict):
|
||||||
LOG.debug("%s does not match the value in the target object:%s",
|
target_value = target_dict.get(self.field)
|
||||||
conv_func(value), target_value)
|
# target_value might be a boolean, explicitly compare with None
|
||||||
return False
|
if target_value is None:
|
||||||
# If we manage to get here, the policy check is successful
|
LOG.debug("Unable to find requested field: %s in target: %s",
|
||||||
return True
|
self.field, target_dict)
|
||||||
|
return False
|
||||||
|
|
||||||
|
return target_value == self.value
|
||||||
|
|
||||||
|
|
||||||
def check(context, action, target, plugin=None):
|
def check(context, action, target, plugin=None):
|
||||||
@ -167,9 +172,9 @@ def check(context, action, target, plugin=None):
|
|||||||
"""
|
"""
|
||||||
init()
|
init()
|
||||||
real_target = _build_target(action, target, plugin, context)
|
real_target = _build_target(action, target, plugin, context)
|
||||||
match_list = _build_match_list(action, real_target)
|
match_rule = _build_match_rule(action, real_target)
|
||||||
credentials = context.to_dict()
|
credentials = context.to_dict()
|
||||||
return policy.enforce(match_list, real_target, credentials)
|
return policy.check(match_rule, real_target, credentials)
|
||||||
|
|
||||||
|
|
||||||
def enforce(context, action, target, plugin=None):
|
def enforce(context, action, target, plugin=None):
|
||||||
@ -189,7 +194,7 @@ def enforce(context, action, target, plugin=None):
|
|||||||
|
|
||||||
init()
|
init()
|
||||||
real_target = _build_target(action, target, plugin, context)
|
real_target = _build_target(action, target, plugin, context)
|
||||||
match_list = _build_match_list(action, real_target)
|
match_rule = _build_match_rule(action, real_target)
|
||||||
credentials = context.to_dict()
|
credentials = context.to_dict()
|
||||||
policy.enforce(match_list, real_target, credentials,
|
return policy.check(match_rule, real_target, credentials,
|
||||||
exceptions.PolicyNotAuthorized, action=action)
|
exceptions.PolicyNotAuthorized, action=action)
|
||||||
|
@ -69,10 +69,10 @@ class PolicyFileTestCase(unittest.TestCase):
|
|||||||
tmpfilename = os.path.join(tmpdir, 'policy')
|
tmpfilename = os.path.join(tmpdir, 'policy')
|
||||||
action = "example:test"
|
action = "example:test"
|
||||||
with open(tmpfilename, "w") as policyfile:
|
with open(tmpfilename, "w") as policyfile:
|
||||||
policyfile.write("""{"example:test": []}""")
|
policyfile.write("""{"example:test": ""}""")
|
||||||
policy.enforce(self.context, action, self.target)
|
policy.enforce(self.context, action, self.target)
|
||||||
with open(tmpfilename, "w") as policyfile:
|
with open(tmpfilename, "w") as policyfile:
|
||||||
policyfile.write("""{"example:test": ["false:false"]}""")
|
policyfile.write("""{"example:test": "!"}""")
|
||||||
# NOTE(vish): reset stored policy cache so we don't have to
|
# NOTE(vish): reset stored policy cache so we don't have to
|
||||||
# sleep(1)
|
# sleep(1)
|
||||||
policy._POLICY_CACHE = {}
|
policy._POLICY_CACHE = {}
|
||||||
@ -90,19 +90,20 @@ class PolicyTestCase(unittest.TestCase):
|
|||||||
# NOTE(vish): preload rules to circumvent reloading from file
|
# NOTE(vish): preload rules to circumvent reloading from file
|
||||||
policy.init()
|
policy.init()
|
||||||
rules = {
|
rules = {
|
||||||
"true": [],
|
"true": '@',
|
||||||
"example:allowed": [],
|
"example:allowed": '@',
|
||||||
"example:denied": [["false:false"]],
|
"example:denied": '!',
|
||||||
"example:get_http": [["http:http://www.example.com"]],
|
"example:get_http": "http:http://www.example.com",
|
||||||
"example:my_file": [["role:compute_admin"],
|
"example:my_file": "role:compute_admin or tenant_id:%(tenant_id)s",
|
||||||
["tenant_id:%(tenant_id)s"]],
|
"example:early_and_fail": "! and @",
|
||||||
"example:early_and_fail": [["false:false", "rule:true"]],
|
"example:early_or_success": "@ or !",
|
||||||
"example:early_or_success": [["rule:true"], ["false:false"]],
|
"example:lowercase_admin": "role:admin or role:sysadmin",
|
||||||
"example:lowercase_admin": [["role:admin"], ["role:sysadmin"]],
|
"example:uppercase_admin": "role:ADMIN or role:sysadmin",
|
||||||
"example:uppercase_admin": [["role:ADMIN"], ["role:sysadmin"]],
|
|
||||||
}
|
}
|
||||||
# NOTE(vish): then overload underlying brain
|
# NOTE(vish): then overload underlying rules
|
||||||
common_policy.set_brain(common_policy.HttpBrain(rules))
|
common_policy.set_rules(common_policy.Rules(
|
||||||
|
dict((k, common_policy.parse_rule(v))
|
||||||
|
for k, v in rules.items())))
|
||||||
self.context = context.Context('fake', 'fake', roles=['member'])
|
self.context = context.Context('fake', 'fake', roles=['member'])
|
||||||
self.target = {}
|
self.target = {}
|
||||||
|
|
||||||
@ -120,9 +121,15 @@ class PolicyTestCase(unittest.TestCase):
|
|||||||
self.assertRaises(exceptions.PolicyNotAuthorized, policy.enforce,
|
self.assertRaises(exceptions.PolicyNotAuthorized, policy.enforce,
|
||||||
self.context, action, self.target)
|
self.context, action, self.target)
|
||||||
|
|
||||||
|
def test_check_bad_action_noraise(self):
|
||||||
|
action = "example:denied"
|
||||||
|
result = policy.check(self.context, action, self.target)
|
||||||
|
self.assertEqual(result, False)
|
||||||
|
|
||||||
def test_enforce_good_action(self):
|
def test_enforce_good_action(self):
|
||||||
action = "example:allowed"
|
action = "example:allowed"
|
||||||
policy.enforce(self.context, action, self.target)
|
result = policy.enforce(self.context, action, self.target)
|
||||||
|
self.assertEqual(result, True)
|
||||||
|
|
||||||
def test_enforce_http_true(self):
|
def test_enforce_http_true(self):
|
||||||
|
|
||||||
@ -133,7 +140,7 @@ class PolicyTestCase(unittest.TestCase):
|
|||||||
action = "example:get_http"
|
action = "example:get_http"
|
||||||
target = {}
|
target = {}
|
||||||
result = policy.enforce(self.context, action, target)
|
result = policy.enforce(self.context, action, target)
|
||||||
self.assertEqual(result, None)
|
self.assertEqual(result, True)
|
||||||
|
|
||||||
def test_enforce_http_false(self):
|
def test_enforce_http_false(self):
|
||||||
|
|
||||||
@ -181,17 +188,19 @@ class DefaultPolicyTestCase(unittest.TestCase):
|
|||||||
policy.init()
|
policy.init()
|
||||||
|
|
||||||
self.rules = {
|
self.rules = {
|
||||||
"default": [],
|
"default": '',
|
||||||
"example:exist": [["false:false"]]
|
"example:exist": '!',
|
||||||
}
|
}
|
||||||
|
|
||||||
self._set_brain('default')
|
self._set_rules('default')
|
||||||
|
|
||||||
self.context = context.Context('fake', 'fake')
|
self.context = context.Context('fake', 'fake')
|
||||||
|
|
||||||
def _set_brain(self, default_rule):
|
def _set_rules(self, default_rule):
|
||||||
brain = common_policy.HttpBrain(self.rules, default_rule)
|
rules = common_policy.Rules(
|
||||||
common_policy.set_brain(brain)
|
dict((k, common_policy.parse_rule(v))
|
||||||
|
for k, v in self.rules.items()), default_rule)
|
||||||
|
common_policy.set_rules(rules)
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
super(DefaultPolicyTestCase, self).tearDown()
|
super(DefaultPolicyTestCase, self).tearDown()
|
||||||
@ -205,7 +214,7 @@ class DefaultPolicyTestCase(unittest.TestCase):
|
|||||||
policy.enforce(self.context, "example:noexist", {})
|
policy.enforce(self.context, "example:noexist", {})
|
||||||
|
|
||||||
def test_default_not_found(self):
|
def test_default_not_found(self):
|
||||||
self._set_brain("default_noexist")
|
self._set_rules("default_noexist")
|
||||||
self.assertRaises(exceptions.PolicyNotAuthorized, policy.enforce,
|
self.assertRaises(exceptions.PolicyNotAuthorized, policy.enforce,
|
||||||
self.context, "example:noexist", {})
|
self.context, "example:noexist", {})
|
||||||
|
|
||||||
@ -216,29 +225,29 @@ class QuantumPolicyTestCase(unittest.TestCase):
|
|||||||
super(QuantumPolicyTestCase, self).setUp()
|
super(QuantumPolicyTestCase, self).setUp()
|
||||||
policy.reset()
|
policy.reset()
|
||||||
policy.init()
|
policy.init()
|
||||||
self.rules = {
|
self.rules = dict((k, common_policy.parse_rule(v)) for k, v in {
|
||||||
"admin_or_network_owner": [["role:admin"],
|
"admin_or_network_owner": "role:admin or "
|
||||||
["tenant_id:%(network_tenant_id)s"]],
|
"tenant_id:%(network_tenant_id)s",
|
||||||
"admin_or_owner": [["role:admin"], ["tenant_id:%(tenant_id)s"]],
|
"admin_or_owner": "role:admin or tenant_id:%(tenant_id)s",
|
||||||
"admin_only": [["role:admin"]],
|
"admin_only": "role:admin",
|
||||||
"regular_user": [["role:user"]],
|
"regular_user": "role:user",
|
||||||
"shared": [["field:networks:shared=True"]],
|
"shared": "field:networks:shared=True",
|
||||||
"external": [["field:networks:router:external=True"]],
|
"external": "field:networks:router:external=True",
|
||||||
"default": [],
|
"default": '@',
|
||||||
|
|
||||||
"create_network": [["rule:admin_or_owner"]],
|
"create_network": "rule:admin_or_owner",
|
||||||
"create_network:shared": [["rule:admin_only"]],
|
"create_network:shared": "rule:admin_only",
|
||||||
"update_network": [],
|
"update_network": '@',
|
||||||
"update_network:shared": [["rule:admin_only"]],
|
"update_network:shared": "rule:admin_only",
|
||||||
|
|
||||||
"get_network": [["rule:admin_or_owner"],
|
"get_network": "rule:admin_or_owner or "
|
||||||
["rule:shared"],
|
"rule:shared or "
|
||||||
["rule:external"]],
|
"rule:external",
|
||||||
"create_port:mac": [["rule:admin_or_network_owner"]],
|
"create_port:mac": "rule:admin_or_network_owner",
|
||||||
}
|
}.items())
|
||||||
|
|
||||||
def fakepolicyinit():
|
def fakepolicyinit():
|
||||||
common_policy.set_brain(common_policy.Brain(self.rules))
|
common_policy.set_rules(common_policy.Rules(self.rules))
|
||||||
|
|
||||||
self.patcher = mock.patch.object(quantum.policy,
|
self.patcher = mock.patch.object(quantum.policy,
|
||||||
'init',
|
'init',
|
||||||
@ -262,7 +271,7 @@ class QuantumPolicyTestCase(unittest.TestCase):
|
|||||||
context, action, target, None)
|
context, action, target, None)
|
||||||
else:
|
else:
|
||||||
result = policy.enforce(context, action, target, None)
|
result = policy.enforce(context, action, target, None)
|
||||||
self.assertEqual(result, None)
|
self.assertEqual(result, True)
|
||||||
|
|
||||||
def _test_nonadmin_action_on_attr(self, action, attr, value,
|
def _test_nonadmin_action_on_attr(self, action, attr, value,
|
||||||
exception=None):
|
exception=None):
|
||||||
@ -289,7 +298,7 @@ class QuantumPolicyTestCase(unittest.TestCase):
|
|||||||
admin_context = context.get_admin_context()
|
admin_context = context.get_admin_context()
|
||||||
target = {'shared': True}
|
target = {'shared': True}
|
||||||
result = policy.enforce(admin_context, action, target, None)
|
result = policy.enforce(admin_context, action, target, None)
|
||||||
self.assertEqual(result, None)
|
self.assertEqual(result, True)
|
||||||
|
|
||||||
def test_enforce_adminonly_attribute_create(self):
|
def test_enforce_adminonly_attribute_create(self):
|
||||||
self._test_enforce_adminonly_attribute('create_network')
|
self._test_enforce_adminonly_attribute('create_network')
|
||||||
@ -297,7 +306,7 @@ class QuantumPolicyTestCase(unittest.TestCase):
|
|||||||
def test_enforce_adminonly_attribute_update(self):
|
def test_enforce_adminonly_attribute_update(self):
|
||||||
self._test_enforce_adminonly_attribute('update_network')
|
self._test_enforce_adminonly_attribute('update_network')
|
||||||
|
|
||||||
def test_enforce_adminoly_attribute_nonadminctx_returns_403(self):
|
def test_enforce_adminonly_attribute_nonadminctx_returns_403(self):
|
||||||
action = "create_network"
|
action = "create_network"
|
||||||
target = {'shared': True, 'tenant_id': 'somebody_else'}
|
target = {'shared': True, 'tenant_id': 'somebody_else'}
|
||||||
self.assertRaises(exceptions.PolicyNotAuthorized, policy.enforce,
|
self.assertRaises(exceptions.PolicyNotAuthorized, policy.enforce,
|
||||||
@ -307,7 +316,7 @@ class QuantumPolicyTestCase(unittest.TestCase):
|
|||||||
action = "get_network"
|
action = "get_network"
|
||||||
target = {'shared': True, 'tenant_id': 'somebody_else'}
|
target = {'shared': True, 'tenant_id': 'somebody_else'}
|
||||||
result = policy.enforce(self.context, action, target, None)
|
result = policy.enforce(self.context, action, target, None)
|
||||||
self.assertIsNone(result)
|
self.assertTrue(result)
|
||||||
|
|
||||||
def test_enforce_parentresource_owner(self):
|
def test_enforce_parentresource_owner(self):
|
||||||
|
|
||||||
@ -318,4 +327,4 @@ class QuantumPolicyTestCase(unittest.TestCase):
|
|||||||
with mock.patch.object(self.plugin, 'get_network', new=fakegetnetwork):
|
with mock.patch.object(self.plugin, 'get_network', new=fakegetnetwork):
|
||||||
target = {'network_id': 'whatever'}
|
target = {'network_id': 'whatever'}
|
||||||
result = policy.enforce(self.context, action, target, self.plugin)
|
result = policy.enforce(self.context, action, target, self.plugin)
|
||||||
self.assertIsNone(result)
|
self.assertTrue(result)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user