Updated policy module from oslo-incubator
Common policy has not been synced with oslo-incubator for a long time and is seriously outdated. This change pulls in fresh code from oslo-incubator which introduces the Enforcer class to replace the old check function. Rewrite neutron.policy using naming conventions and approach that was set in Nova and amend related unit tests. Remove neutron.common.exceptions.PolicyNotAuthorized and switch to neutron.openstack.common.policy.PolicyNotAuthorized. Drop Neutron specific policy_file option since now it is defined in oslo-incubator policy module. Change log: 4ca5091 Fixes nits in module policy 262fc82 Correct default rule name for policy.Enforcer 9e8b9f6 Minor fixes in policy module 6c706c5 Delete graduated serialization files 5d40e14 Remove code that moved to oslo.i18n aebb58f Fix typo to show correct log message bb410d9 Use MultiStrOpt for policy_dirs 33f44bf Add support for policy configration directories 2b966f9 Fix deletion of cached file for policy enforcer 238e601 Make policy debug logging less verbose fe3389e Improve help strings 15722f1 Adds a flag to determine whether to reload the rules in policy 5d1f15a Documenting policy.json syntax fcf517d Update oslo log messages with translation domains e038d89 Fix policy tests for parallel testing 0da5de6 Allow policy.json resource vs constant check e4b2334 Replaces use of urlutils with six in policy module 8b2b0b7 Use hacking import_exceptions for gettextutils._ 0d8f18b Use urlutils functions instead of urllib/urllib2 12bcdb7 Remove vim header 9ef9fec Use six.string_type instead of basestring 4bfb7a2 Apply six for metaclass 1538c80 ConfigFileNotFoundError with proper argument 33533b0 Keystone user can't perform revoke_token 64bb5e2 Fix wrong argument in openstack common policy b7edc99 Fix missing argument bug in oslo common policy 3626b6d Fix policy default_rule issue 7bf8ee9 Allow use of hacking 0.6.0 and enable new checks e4ac367 Fix missing argument bug in oslo common policy 1a2df89 Enable H302 hacking check 7119e29 Enable hacking H404 test. 6d27681 Enable H306 hacking check. 1091b4f Reduce duplicated code related to policies Closes-Bug: #1288178 Change-Id: I87ee30e2b64ec6b07faa84a231fd5f7eb925d501
This commit is contained in:
parent
b590218598
commit
ad6bfc1ab6
@ -28,6 +28,7 @@ from neutron.common import exceptions
|
|||||||
from neutron.common import rpc as n_rpc
|
from neutron.common import rpc as n_rpc
|
||||||
from neutron.openstack.common import excutils
|
from neutron.openstack.common import excutils
|
||||||
from neutron.openstack.common import log as logging
|
from neutron.openstack.common import log as logging
|
||||||
|
from neutron.openstack.common import policy as common_policy
|
||||||
from neutron import policy
|
from neutron import policy
|
||||||
from neutron import quota
|
from neutron import quota
|
||||||
|
|
||||||
@ -41,6 +42,7 @@ FAULT_MAP = {exceptions.NotFound: webob.exc.HTTPNotFound,
|
|||||||
exceptions.ServiceUnavailable: webob.exc.HTTPServiceUnavailable,
|
exceptions.ServiceUnavailable: webob.exc.HTTPServiceUnavailable,
|
||||||
exceptions.NotAuthorized: webob.exc.HTTPForbidden,
|
exceptions.NotAuthorized: webob.exc.HTTPForbidden,
|
||||||
netaddr.AddrFormatError: webob.exc.HTTPBadRequest,
|
netaddr.AddrFormatError: webob.exc.HTTPBadRequest,
|
||||||
|
common_policy.PolicyNotAuthorized: webob.exc.HTTPForbidden
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -187,7 +189,7 @@ class Controller(object):
|
|||||||
# Fetch the resource and verify if the user can access it
|
# Fetch the resource and verify if the user can access it
|
||||||
try:
|
try:
|
||||||
resource = self._item(request, id, True)
|
resource = self._item(request, id, True)
|
||||||
except exceptions.PolicyNotAuthorized:
|
except common_policy.PolicyNotAuthorized:
|
||||||
msg = _('The resource could not be found.')
|
msg = _('The resource could not be found.')
|
||||||
raise webob.exc.HTTPNotFound(msg)
|
raise webob.exc.HTTPNotFound(msg)
|
||||||
body = kwargs.pop('body', None)
|
body = kwargs.pop('body', None)
|
||||||
@ -326,7 +328,7 @@ class Controller(object):
|
|||||||
field_list=field_list,
|
field_list=field_list,
|
||||||
parent_id=parent_id),
|
parent_id=parent_id),
|
||||||
fields_to_strip=added_fields)}
|
fields_to_strip=added_fields)}
|
||||||
except exceptions.PolicyNotAuthorized:
|
except common_policy.PolicyNotAuthorized:
|
||||||
# To avoid giving away information, pretend that it
|
# To avoid giving away information, pretend that it
|
||||||
# doesn't exist
|
# doesn't exist
|
||||||
msg = _('The resource could not be found.')
|
msg = _('The resource could not be found.')
|
||||||
@ -466,7 +468,7 @@ class Controller(object):
|
|||||||
policy.enforce(request.context,
|
policy.enforce(request.context,
|
||||||
action,
|
action,
|
||||||
obj)
|
obj)
|
||||||
except exceptions.PolicyNotAuthorized:
|
except common_policy.PolicyNotAuthorized:
|
||||||
# To avoid giving away information, pretend that it
|
# To avoid giving away information, pretend that it
|
||||||
# doesn't exist
|
# doesn't exist
|
||||||
msg = _('The resource could not be found.')
|
msg = _('The resource could not be found.')
|
||||||
@ -521,7 +523,7 @@ class Controller(object):
|
|||||||
policy.enforce(request.context,
|
policy.enforce(request.context,
|
||||||
action,
|
action,
|
||||||
orig_obj)
|
orig_obj)
|
||||||
except exceptions.PolicyNotAuthorized:
|
except common_policy.PolicyNotAuthorized:
|
||||||
with excutils.save_and_reraise_exception() as ctxt:
|
with excutils.save_and_reraise_exception() as ctxt:
|
||||||
# If a tenant is modifying it's own object, it's safe to return
|
# If a tenant is modifying it's own object, it's safe to return
|
||||||
# a 403. Otherwise, pretend that it doesn't exist to avoid
|
# a 403. Otherwise, pretend that it doesn't exist to avoid
|
||||||
|
@ -27,6 +27,7 @@ import webob.exc
|
|||||||
from neutron.common import exceptions
|
from neutron.common import exceptions
|
||||||
from neutron.openstack.common import gettextutils
|
from neutron.openstack.common import gettextutils
|
||||||
from neutron.openstack.common import log as logging
|
from neutron.openstack.common import log as logging
|
||||||
|
from neutron.openstack.common import policy as common_policy
|
||||||
from neutron import wsgi
|
from neutron import wsgi
|
||||||
|
|
||||||
|
|
||||||
@ -80,7 +81,8 @@ def Resource(controller, faults=None, deserializers=None, serializers=None):
|
|||||||
|
|
||||||
result = method(request=request, **args)
|
result = method(request=request, **args)
|
||||||
except (exceptions.NeutronException,
|
except (exceptions.NeutronException,
|
||||||
netaddr.AddrFormatError) as e:
|
netaddr.AddrFormatError,
|
||||||
|
common_policy.PolicyNotAuthorized) as e:
|
||||||
for fault in faults:
|
for fault in faults:
|
||||||
if isinstance(e, fault):
|
if isinstance(e, fault):
|
||||||
mapped_exc = faults[fault]
|
mapped_exc = faults[fault]
|
||||||
|
@ -41,8 +41,6 @@ core_opts = [
|
|||||||
help=_("The API paste config file to use")),
|
help=_("The API paste config file to use")),
|
||||||
cfg.StrOpt('api_extensions_path', default="",
|
cfg.StrOpt('api_extensions_path', default="",
|
||||||
help=_("The path for API extensions")),
|
help=_("The path for API extensions")),
|
||||||
cfg.StrOpt('policy_file', default="policy.json",
|
|
||||||
help=_("The policy file to use")),
|
|
||||||
cfg.StrOpt('auth_strategy', default='keystone',
|
cfg.StrOpt('auth_strategy', default='keystone',
|
||||||
help=_("The type of authentication to use")),
|
help=_("The type of authentication to use")),
|
||||||
cfg.StrOpt('core_plugin',
|
cfg.StrOpt('core_plugin',
|
||||||
|
@ -71,10 +71,6 @@ class AdminRequired(NotAuthorized):
|
|||||||
message = _("User does not have admin privileges: %(reason)s")
|
message = _("User does not have admin privileges: %(reason)s")
|
||||||
|
|
||||||
|
|
||||||
class PolicyNotAuthorized(NotAuthorized):
|
|
||||||
message = _("Policy doesn't allow %(action)s to be performed.")
|
|
||||||
|
|
||||||
|
|
||||||
class NetworkNotFound(NotFound):
|
class NetworkNotFound(NotFound):
|
||||||
message = _("Network %(net_id)s could not be found")
|
message = _("Network %(net_id)s could not be found")
|
||||||
|
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
|
||||||
|
|
||||||
# Copyright (c) 2012 OpenStack Foundation.
|
# Copyright (c) 2012 OpenStack Foundation.
|
||||||
# All Rights Reserved.
|
# All Rights Reserved.
|
||||||
#
|
#
|
||||||
@ -48,6 +46,27 @@ policy rule::
|
|||||||
|
|
||||||
project_id:%(project_id)s and not role:dunce
|
project_id:%(project_id)s and not role:dunce
|
||||||
|
|
||||||
|
It is possible to perform policy checks on the following user
|
||||||
|
attributes (obtained through the token): user_id, domain_id or
|
||||||
|
project_id::
|
||||||
|
|
||||||
|
domain_id:<some_value>
|
||||||
|
|
||||||
|
Attributes sent along with API calls can be used by the policy engine
|
||||||
|
(on the right side of the expression), by using the following syntax::
|
||||||
|
|
||||||
|
<some_value>:user.id
|
||||||
|
|
||||||
|
Contextual attributes of objects identified by their IDs are loaded
|
||||||
|
from the database. They are also available to the policy engine and
|
||||||
|
can be checked through the `target` keyword::
|
||||||
|
|
||||||
|
<some_value>:target.role.name
|
||||||
|
|
||||||
|
All these attributes (related to users, API calls, and context) can be
|
||||||
|
checked against each other or against constants, be it literals (True,
|
||||||
|
<a_number>) or strings.
|
||||||
|
|
||||||
Finally, two special policy checks should be mentioned; the policy
|
Finally, two special policy checks should be mentioned; the policy
|
||||||
check "@" will always accept an access, and the policy check "!" will
|
check "@" will always accept an access, and the policy check "!" will
|
||||||
always reject an access. (Note that if a rule is either the empty
|
always reject an access. (Note that if a rule is either the empty
|
||||||
@ -57,34 +76,56 @@ as it allows particular rules to be explicitly disabled.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import abc
|
import abc
|
||||||
|
import ast
|
||||||
|
import os
|
||||||
import re
|
import re
|
||||||
import urllib
|
|
||||||
|
|
||||||
|
from oslo.config import cfg
|
||||||
|
from oslo.serialization import jsonutils
|
||||||
import six
|
import six
|
||||||
import urllib2
|
import six.moves.urllib.parse as urlparse
|
||||||
|
import six.moves.urllib.request as urlrequest
|
||||||
|
|
||||||
from neutron.openstack.common.gettextutils import _
|
from neutron.openstack.common import fileutils
|
||||||
from neutron.openstack.common import jsonutils
|
from neutron.openstack.common._i18n import _, _LE, _LW
|
||||||
from neutron.openstack.common import log as logging
|
from neutron.openstack.common import log as logging
|
||||||
|
|
||||||
|
|
||||||
|
policy_opts = [
|
||||||
|
cfg.StrOpt('policy_file',
|
||||||
|
default='policy.json',
|
||||||
|
help=_('The JSON file that defines policies.')),
|
||||||
|
cfg.StrOpt('policy_default_rule',
|
||||||
|
default='default',
|
||||||
|
help=_('Default rule. Enforced when a requested rule is not '
|
||||||
|
'found.')),
|
||||||
|
cfg.MultiStrOpt('policy_dirs',
|
||||||
|
default=['policy.d'],
|
||||||
|
help=_('Directories where policy configuration files are '
|
||||||
|
'stored.')),
|
||||||
|
]
|
||||||
|
|
||||||
|
CONF = cfg.CONF
|
||||||
|
CONF.register_opts(policy_opts)
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
_rules = None
|
|
||||||
_checks = {}
|
_checks = {}
|
||||||
|
|
||||||
|
|
||||||
|
class PolicyNotAuthorized(Exception):
|
||||||
|
|
||||||
|
def __init__(self, rule):
|
||||||
|
msg = _("Policy doesn't allow %s to be performed.") % rule
|
||||||
|
super(PolicyNotAuthorized, self).__init__(msg)
|
||||||
|
|
||||||
|
|
||||||
class Rules(dict):
|
class Rules(dict):
|
||||||
"""
|
"""A store for rules. Handles the default_rule setting directly."""
|
||||||
A store for rules. Handles the default_rule setting directly.
|
|
||||||
"""
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def load_json(cls, data, default_rule=None):
|
def load_json(cls, data, default_rule=None):
|
||||||
"""
|
"""Allow loading of JSON rule data."""
|
||||||
Allow loading of JSON rule data.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Suck in the JSON data and parse the rules
|
# Suck in the JSON data and parse the rules
|
||||||
rules = dict((k, parse_rule(v)) for k, v in
|
rules = dict((k, parse_rule(v)) for k, v in
|
||||||
@ -101,12 +142,23 @@ class Rules(dict):
|
|||||||
def __missing__(self, key):
|
def __missing__(self, key):
|
||||||
"""Implements the default rule handling."""
|
"""Implements the default rule handling."""
|
||||||
|
|
||||||
# If the default rule isn't actually defined, do something
|
if isinstance(self.default_rule, dict):
|
||||||
# reasonably intelligent
|
|
||||||
if not self.default_rule or self.default_rule not in self:
|
|
||||||
raise KeyError(key)
|
raise KeyError(key)
|
||||||
|
|
||||||
return self[self.default_rule]
|
# If the default rule isn't actually defined, do something
|
||||||
|
# reasonably intelligent
|
||||||
|
if not self.default_rule:
|
||||||
|
raise KeyError(key)
|
||||||
|
|
||||||
|
if isinstance(self.default_rule, BaseCheck):
|
||||||
|
return self.default_rule
|
||||||
|
|
||||||
|
# We need to check this or we can get infinite recursion
|
||||||
|
if self.default_rule not in self:
|
||||||
|
raise KeyError(key)
|
||||||
|
|
||||||
|
elif isinstance(self.default_rule, six.string_types):
|
||||||
|
return self[self.default_rule]
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
"""Dumps a string representation of the rules."""
|
"""Dumps a string representation of the rules."""
|
||||||
@ -124,87 +176,183 @@ class Rules(dict):
|
|||||||
return jsonutils.dumps(out_rules, indent=4)
|
return jsonutils.dumps(out_rules, indent=4)
|
||||||
|
|
||||||
|
|
||||||
# Really have to figure out a way to deprecate this
|
class Enforcer(object):
|
||||||
def set_rules(rules):
|
"""Responsible for loading and enforcing rules.
|
||||||
"""Set the rules in use for policy checks."""
|
|
||||||
|
|
||||||
global _rules
|
:param policy_file: Custom policy file to use, if none is
|
||||||
|
specified, `CONF.policy_file` will be
|
||||||
_rules = rules
|
used.
|
||||||
|
:param rules: Default dictionary / Rules to use. It will be
|
||||||
|
considered just in the first instantiation. If
|
||||||
# Ditto
|
`load_rules(True)`, `clear()` or `set_rules(True)`
|
||||||
def reset():
|
is called this will be overwritten.
|
||||||
"""Clear the rules used for policy checks."""
|
:param default_rule: Default rule to use, CONF.default_rule will
|
||||||
|
be used if none is specified.
|
||||||
global _rules
|
:param use_conf: Whether to load rules from cache or config file.
|
||||||
|
|
||||||
_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
|
def __init__(self, policy_file=None, rules=None,
|
||||||
if isinstance(rule, BaseCheck):
|
default_rule=None, use_conf=True):
|
||||||
result = rule(target, creds)
|
self.default_rule = default_rule or CONF.policy_default_rule
|
||||||
elif not _rules:
|
self.rules = Rules(rules, self.default_rule)
|
||||||
# No rules to reference means we're going to fail closed
|
|
||||||
result = False
|
self.policy_path = None
|
||||||
else:
|
self.policy_file = policy_file or CONF.policy_file
|
||||||
try:
|
self.use_conf = use_conf
|
||||||
# Evaluate the rule
|
|
||||||
result = _rules[rule](target, creds)
|
def set_rules(self, rules, overwrite=True, use_conf=False):
|
||||||
except KeyError:
|
"""Create a new Rules object based on the provided dict of rules.
|
||||||
# If the rule doesn't exist, fail closed
|
|
||||||
|
:param rules: New rules to use. It should be an instance of dict.
|
||||||
|
:param overwrite: Whether to overwrite current rules or update them
|
||||||
|
with the new rules.
|
||||||
|
:param use_conf: Whether to reload rules from cache or config file.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not isinstance(rules, dict):
|
||||||
|
raise TypeError(_("Rules must be an instance of dict or Rules, "
|
||||||
|
"got %s instead") % type(rules))
|
||||||
|
self.use_conf = use_conf
|
||||||
|
if overwrite:
|
||||||
|
self.rules = Rules(rules, self.default_rule)
|
||||||
|
else:
|
||||||
|
self.rules.update(rules)
|
||||||
|
|
||||||
|
def clear(self):
|
||||||
|
"""Clears Enforcer rules, policy's cache and policy's path."""
|
||||||
|
self.set_rules({})
|
||||||
|
fileutils.delete_cached_file(self.policy_path)
|
||||||
|
self.default_rule = None
|
||||||
|
self.policy_path = None
|
||||||
|
|
||||||
|
def load_rules(self, force_reload=False):
|
||||||
|
"""Loads policy_path's rules.
|
||||||
|
|
||||||
|
Policy file is cached and will be reloaded if modified.
|
||||||
|
|
||||||
|
:param force_reload: Whether to overwrite current rules.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if force_reload:
|
||||||
|
self.use_conf = force_reload
|
||||||
|
|
||||||
|
if self.use_conf:
|
||||||
|
if not self.policy_path:
|
||||||
|
self.policy_path = self._get_policy_path(self.policy_file)
|
||||||
|
|
||||||
|
self._load_policy_file(self.policy_path, force_reload)
|
||||||
|
for path in CONF.policy_dirs:
|
||||||
|
try:
|
||||||
|
path = self._get_policy_path(path)
|
||||||
|
except cfg.ConfigFilesNotFoundError:
|
||||||
|
LOG.warn(_LW("Can not find policy directory: %s"), path)
|
||||||
|
continue
|
||||||
|
self._walk_through_policy_directory(path,
|
||||||
|
self._load_policy_file,
|
||||||
|
force_reload, False)
|
||||||
|
|
||||||
|
def _walk_through_policy_directory(self, path, func, *args):
|
||||||
|
# We do not iterate over sub-directories.
|
||||||
|
policy_files = next(os.walk(path))[2]
|
||||||
|
policy_files.sort()
|
||||||
|
for policy_file in [p for p in policy_files if not p.startswith('.')]:
|
||||||
|
func(os.path.join(path, policy_file), *args)
|
||||||
|
|
||||||
|
def _load_policy_file(self, path, force_reload, overwrite=True):
|
||||||
|
reloaded, data = fileutils.read_cached_file(
|
||||||
|
path, force_reload=force_reload)
|
||||||
|
if reloaded or not self.rules:
|
||||||
|
rules = Rules.load_json(data, self.default_rule)
|
||||||
|
self.set_rules(rules, overwrite)
|
||||||
|
LOG.debug("Rules successfully reloaded")
|
||||||
|
|
||||||
|
def _get_policy_path(self, path):
|
||||||
|
"""Locate the policy json data file/path.
|
||||||
|
|
||||||
|
:param path: It's value can be a full path or related path. When
|
||||||
|
full path specified, this function just returns the full
|
||||||
|
path. When related path specified, this function will
|
||||||
|
search configuration directories to find one that exists.
|
||||||
|
|
||||||
|
:returns: The policy path
|
||||||
|
|
||||||
|
:raises: ConfigFilesNotFoundError if the file/path couldn't
|
||||||
|
be located.
|
||||||
|
"""
|
||||||
|
policy_path = CONF.find_file(path)
|
||||||
|
|
||||||
|
if policy_path:
|
||||||
|
return policy_path
|
||||||
|
|
||||||
|
raise cfg.ConfigFilesNotFoundError((path,))
|
||||||
|
|
||||||
|
def enforce(self, rule, target, creds, do_raise=False,
|
||||||
|
exc=None, *args, **kwargs):
|
||||||
|
"""Checks authorization of a rule against the target and credentials.
|
||||||
|
|
||||||
|
:param rule: A string or BaseCheck instance specifying 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 do_raise: Whether to raise an exception or not if check
|
||||||
|
fails.
|
||||||
|
:param exc: 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 not specified, PolicyNotAuthorized
|
||||||
|
will be used.
|
||||||
|
|
||||||
|
: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.
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.load_rules()
|
||||||
|
|
||||||
|
# Allow the rule to be a Check tree
|
||||||
|
if isinstance(rule, BaseCheck):
|
||||||
|
result = rule(target, creds, self)
|
||||||
|
elif not self.rules:
|
||||||
|
# No rules to reference means we're going to fail closed
|
||||||
result = False
|
result = False
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
# Evaluate the rule
|
||||||
|
result = self.rules[rule](target, creds, self)
|
||||||
|
except KeyError:
|
||||||
|
LOG.debug("Rule [%s] doesn't exist" % rule)
|
||||||
|
# If the rule doesn't exist, fail closed
|
||||||
|
result = False
|
||||||
|
|
||||||
# If it is False, raise the exception if requested
|
# If it is False, raise the exception if requested
|
||||||
if exc and result is False:
|
if do_raise and not result:
|
||||||
raise exc(*args, **kwargs)
|
if exc:
|
||||||
|
raise exc(*args, **kwargs)
|
||||||
|
|
||||||
return result
|
raise PolicyNotAuthorized(rule)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
@six.add_metaclass(abc.ABCMeta)
|
||||||
class BaseCheck(object):
|
class BaseCheck(object):
|
||||||
"""
|
"""Abstract base class for Check classes."""
|
||||||
Abstract base class for Check classes.
|
|
||||||
"""
|
|
||||||
|
|
||||||
__metaclass__ = abc.ABCMeta
|
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
"""
|
"""String representation of the Check tree rooted at this node."""
|
||||||
Retrieve a string representation of the Check tree rooted at
|
|
||||||
this node.
|
|
||||||
"""
|
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def __call__(self, target, cred):
|
def __call__(self, target, cred, enforcer):
|
||||||
"""
|
"""Triggers if instance of the class is called.
|
||||||
Perform the check. Returns False to reject the access or a
|
|
||||||
|
Performs the check. Returns False to reject the access or a
|
||||||
true value (not necessary True) to accept the access.
|
true value (not necessary True) to accept the access.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@ -212,44 +360,39 @@ class BaseCheck(object):
|
|||||||
|
|
||||||
|
|
||||||
class FalseCheck(BaseCheck):
|
class FalseCheck(BaseCheck):
|
||||||
"""
|
"""A policy check that always returns False (disallow)."""
|
||||||
A policy check that always returns False (disallow).
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
"""Return a string representation of this check."""
|
"""Return a string representation of this check."""
|
||||||
|
|
||||||
return "!"
|
return "!"
|
||||||
|
|
||||||
def __call__(self, target, cred):
|
def __call__(self, target, cred, enforcer):
|
||||||
"""Check the policy."""
|
"""Check the policy."""
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
class TrueCheck(BaseCheck):
|
class TrueCheck(BaseCheck):
|
||||||
"""
|
"""A policy check that always returns True (allow)."""
|
||||||
A policy check that always returns True (allow).
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
"""Return a string representation of this check."""
|
"""Return a string representation of this check."""
|
||||||
|
|
||||||
return "@"
|
return "@"
|
||||||
|
|
||||||
def __call__(self, target, cred):
|
def __call__(self, target, cred, enforcer):
|
||||||
"""Check the policy."""
|
"""Check the policy."""
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
class Check(BaseCheck):
|
class Check(BaseCheck):
|
||||||
"""
|
"""A base class to allow for user-defined policy checks."""
|
||||||
A base class to allow for user-defined policy checks.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, kind, match):
|
def __init__(self, kind, match):
|
||||||
"""
|
"""Initiates Check instance.
|
||||||
|
|
||||||
:param kind: The kind of the check, i.e., the field before the
|
: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
|
:param match: The match of the check, i.e., the field after
|
||||||
@ -266,14 +409,13 @@ class Check(BaseCheck):
|
|||||||
|
|
||||||
|
|
||||||
class NotCheck(BaseCheck):
|
class NotCheck(BaseCheck):
|
||||||
"""
|
"""Implements the "not" logical operator.
|
||||||
|
|
||||||
A policy check that inverts the result of another policy check.
|
A policy check that inverts the result of another policy check.
|
||||||
Implements the "not" operator.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, rule):
|
def __init__(self, rule):
|
||||||
"""
|
"""Initialize the 'not' check.
|
||||||
Initialize the 'not' check.
|
|
||||||
|
|
||||||
:param rule: The rule to negate. Must be a Check.
|
:param rule: The rule to negate. Must be a Check.
|
||||||
"""
|
"""
|
||||||
@ -285,24 +427,23 @@ class NotCheck(BaseCheck):
|
|||||||
|
|
||||||
return "not %s" % self.rule
|
return "not %s" % self.rule
|
||||||
|
|
||||||
def __call__(self, target, cred):
|
def __call__(self, target, cred, enforcer):
|
||||||
"""
|
"""Check the policy.
|
||||||
Check the policy. Returns the logical inverse of the wrapped
|
|
||||||
check.
|
Returns the logical inverse of the wrapped check.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return not self.rule(target, cred)
|
return not self.rule(target, cred, enforcer)
|
||||||
|
|
||||||
|
|
||||||
class AndCheck(BaseCheck):
|
class AndCheck(BaseCheck):
|
||||||
"""
|
"""Implements the "and" logical operator.
|
||||||
A policy check that requires that a list of other checks all
|
|
||||||
return True. Implements the "and" operator.
|
A policy check that requires that a list of other checks all return True.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, rules):
|
def __init__(self, rules):
|
||||||
"""
|
"""Initialize the 'and' check.
|
||||||
Initialize the 'and' check.
|
|
||||||
|
|
||||||
:param rules: A list of rules that will be tested.
|
:param rules: A list of rules that will be tested.
|
||||||
"""
|
"""
|
||||||
@ -314,20 +455,21 @@ class AndCheck(BaseCheck):
|
|||||||
|
|
||||||
return "(%s)" % ' and '.join(str(r) for r in self.rules)
|
return "(%s)" % ' and '.join(str(r) for r in self.rules)
|
||||||
|
|
||||||
def __call__(self, target, cred):
|
def __call__(self, target, cred, enforcer):
|
||||||
"""
|
"""Check the policy.
|
||||||
Check the policy. Requires that all rules accept in order to
|
|
||||||
return True.
|
Requires that all rules accept in order to return True.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
for rule in self.rules:
|
for rule in self.rules:
|
||||||
if not rule(target, cred):
|
if not rule(target, cred, enforcer):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def add_check(self, rule):
|
def add_check(self, rule):
|
||||||
"""
|
"""Adds rule to be tested.
|
||||||
|
|
||||||
Allows addition of another rule to the list of rules that will
|
Allows addition of another rule to the list of rules that will
|
||||||
be tested. Returns the AndCheck object for convenience.
|
be tested. Returns the AndCheck object for convenience.
|
||||||
"""
|
"""
|
||||||
@ -337,14 +479,14 @@ class AndCheck(BaseCheck):
|
|||||||
|
|
||||||
|
|
||||||
class OrCheck(BaseCheck):
|
class OrCheck(BaseCheck):
|
||||||
"""
|
"""Implements the "or" operator.
|
||||||
|
|
||||||
A policy check that requires that at least one of a list of other
|
A policy check that requires that at least one of a list of other
|
||||||
checks returns True. Implements the "or" operator.
|
checks returns True.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, rules):
|
def __init__(self, rules):
|
||||||
"""
|
"""Initialize the 'or' check.
|
||||||
Initialize the 'or' check.
|
|
||||||
|
|
||||||
:param rules: A list of rules that will be tested.
|
:param rules: A list of rules that will be tested.
|
||||||
"""
|
"""
|
||||||
@ -356,20 +498,20 @@ class OrCheck(BaseCheck):
|
|||||||
|
|
||||||
return "(%s)" % ' or '.join(str(r) for r in self.rules)
|
return "(%s)" % ' or '.join(str(r) for r in self.rules)
|
||||||
|
|
||||||
def __call__(self, target, cred):
|
def __call__(self, target, cred, enforcer):
|
||||||
"""
|
"""Check the policy.
|
||||||
Check the policy. Requires that at least one rule accept in
|
|
||||||
order to return True.
|
Requires that at least one rule accept in order to return True.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
for rule in self.rules:
|
for rule in self.rules:
|
||||||
if rule(target, cred):
|
if rule(target, cred, enforcer):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def add_check(self, rule):
|
def add_check(self, rule):
|
||||||
"""
|
"""Adds rule to be tested.
|
||||||
|
|
||||||
Allows addition of another rule to the list of rules that will
|
Allows addition of another rule to the list of rules that will
|
||||||
be tested. Returns the OrCheck object for convenience.
|
be tested. Returns the OrCheck object for convenience.
|
||||||
"""
|
"""
|
||||||
@ -379,9 +521,7 @@ class OrCheck(BaseCheck):
|
|||||||
|
|
||||||
|
|
||||||
def _parse_check(rule):
|
def _parse_check(rule):
|
||||||
"""
|
"""Parse a single base check rule into an appropriate Check object."""
|
||||||
Parse a single base check rule into an appropriate Check object.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Handle the special checks
|
# Handle the special checks
|
||||||
if rule == '!':
|
if rule == '!':
|
||||||
@ -392,7 +532,7 @@ def _parse_check(rule):
|
|||||||
try:
|
try:
|
||||||
kind, match = rule.split(':', 1)
|
kind, match = rule.split(':', 1)
|
||||||
except Exception:
|
except Exception:
|
||||||
LOG.exception(_("Failed to understand rule %(rule)s") % locals())
|
LOG.exception(_LE("Failed to understand rule %s") % rule)
|
||||||
# If the rule is invalid, we'll fail closed
|
# If the rule is invalid, we'll fail closed
|
||||||
return FalseCheck()
|
return FalseCheck()
|
||||||
|
|
||||||
@ -402,14 +542,14 @@ def _parse_check(rule):
|
|||||||
elif None in _checks:
|
elif None in _checks:
|
||||||
return _checks[None](kind, match)
|
return _checks[None](kind, match)
|
||||||
else:
|
else:
|
||||||
LOG.error(_("No handler for matches of kind %s") % kind)
|
LOG.error(_LE("No handler for matches of kind %s") % kind)
|
||||||
return FalseCheck()
|
return FalseCheck()
|
||||||
|
|
||||||
|
|
||||||
def _parse_list_rule(rule):
|
def _parse_list_rule(rule):
|
||||||
"""
|
"""Translates the old list-of-lists syntax into a tree of Check objects.
|
||||||
Provided for backwards compatibility. Translates the old
|
|
||||||
list-of-lists syntax into a tree of Check objects.
|
Provided for backwards compatibility.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Empty rule defaults to True
|
# Empty rule defaults to True
|
||||||
@ -424,7 +564,7 @@ def _parse_list_rule(rule):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
# Handle bare strings
|
# Handle bare strings
|
||||||
if isinstance(inner_rule, basestring):
|
if isinstance(inner_rule, six.string_types):
|
||||||
inner_rule = [inner_rule]
|
inner_rule = [inner_rule]
|
||||||
|
|
||||||
# Parse the inner rules into Check objects
|
# Parse the inner rules into Check objects
|
||||||
@ -450,8 +590,7 @@ _tokenize_re = re.compile(r'\s+')
|
|||||||
|
|
||||||
|
|
||||||
def _parse_tokenize(rule):
|
def _parse_tokenize(rule):
|
||||||
"""
|
"""Tokenizer for the policy language.
|
||||||
Tokenizer for the policy language.
|
|
||||||
|
|
||||||
Most of the single-character tokens are specified in the
|
Most of the single-character tokens are specified in the
|
||||||
_tokenize_re; however, parentheses need to be handled specially,
|
_tokenize_re; however, parentheses need to be handled specially,
|
||||||
@ -500,16 +639,16 @@ def _parse_tokenize(rule):
|
|||||||
|
|
||||||
|
|
||||||
class ParseStateMeta(type):
|
class ParseStateMeta(type):
|
||||||
"""
|
"""Metaclass for the ParseState class.
|
||||||
Metaclass for the ParseState class. Facilitates identifying
|
|
||||||
reduction methods.
|
Facilitates identifying reduction methods.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __new__(mcs, name, bases, cls_dict):
|
def __new__(mcs, name, bases, cls_dict):
|
||||||
"""
|
"""Create the class.
|
||||||
Create the class. Injects the 'reducers' list, a list of
|
|
||||||
tuples matching token sequences to the names of the
|
Injects the 'reducers' list, a list of tuples matching token sequences
|
||||||
corresponding reduction methods.
|
to the names of the corresponding reduction methods.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
reducers = []
|
reducers = []
|
||||||
@ -526,10 +665,10 @@ class ParseStateMeta(type):
|
|||||||
|
|
||||||
|
|
||||||
def reducer(*tokens):
|
def reducer(*tokens):
|
||||||
"""
|
"""Decorator for reduction methods.
|
||||||
Decorator for reduction methods. Arguments are a sequence of
|
|
||||||
tokens, in order, which should trigger running this reduction
|
Arguments are a sequence of tokens, in order, which should trigger running
|
||||||
method.
|
this reduction method.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def decorator(func):
|
def decorator(func):
|
||||||
@ -545,11 +684,12 @@ def reducer(*tokens):
|
|||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
@six.add_metaclass(ParseStateMeta)
|
||||||
class ParseState(object):
|
class ParseState(object):
|
||||||
"""
|
"""Implement the core of parsing the policy language.
|
||||||
Implement the core of parsing the policy language. Uses a greedy
|
|
||||||
reduction algorithm to reduce a sequence of tokens into a single
|
Uses a greedy reduction algorithm to reduce a sequence of tokens into
|
||||||
terminal, the value of which will be the root of the Check tree.
|
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
|
Note: error reporting is rather lacking. The best we can get with
|
||||||
this parser formulation is an overall "parse failed" error.
|
this parser formulation is an overall "parse failed" error.
|
||||||
@ -557,8 +697,6 @@ class ParseState(object):
|
|||||||
shouldn't be that big a problem.
|
shouldn't be that big a problem.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__metaclass__ = ParseStateMeta
|
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
"""Initialize the ParseState."""
|
"""Initialize the ParseState."""
|
||||||
|
|
||||||
@ -566,11 +704,11 @@ class ParseState(object):
|
|||||||
self.values = []
|
self.values = []
|
||||||
|
|
||||||
def reduce(self):
|
def reduce(self):
|
||||||
"""
|
"""Perform a greedy reduction of the token stream.
|
||||||
Perform a greedy reduction of the token stream. If a reducer
|
|
||||||
method matches, it will be executed, then the reduce() method
|
If a reducer method matches, it will be executed, then the
|
||||||
will be called recursively to search for any more possible
|
reduce() method will be called recursively to search for any more
|
||||||
reductions.
|
possible reductions.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
for reduction, methname in self.reducers:
|
for reduction, methname in self.reducers:
|
||||||
@ -600,9 +738,9 @@ class ParseState(object):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def result(self):
|
def result(self):
|
||||||
"""
|
"""Obtain the final result of the parse.
|
||||||
Obtain the final result of the parse. Raises ValueError if
|
|
||||||
the parse failed to reduce to a single result.
|
Raises ValueError if the parse failed to reduce to a single result.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if len(self.values) != 1:
|
if len(self.values) != 1:
|
||||||
@ -619,35 +757,31 @@ class ParseState(object):
|
|||||||
|
|
||||||
@reducer('check', 'and', 'check')
|
@reducer('check', 'and', 'check')
|
||||||
def _make_and_expr(self, check1, _and, check2):
|
def _make_and_expr(self, check1, _and, check2):
|
||||||
"""
|
"""Create an 'and_expr'.
|
||||||
Create an 'and_expr' from two checks joined by the 'and'
|
|
||||||
operator.
|
Join two checks by the 'and' operator.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return [('and_expr', AndCheck([check1, check2]))]
|
return [('and_expr', AndCheck([check1, check2]))]
|
||||||
|
|
||||||
@reducer('and_expr', 'and', 'check')
|
@reducer('and_expr', 'and', 'check')
|
||||||
def _extend_and_expr(self, and_expr, _and, check):
|
def _extend_and_expr(self, and_expr, _and, check):
|
||||||
"""
|
"""Extend an 'and_expr' by adding one more check."""
|
||||||
Extend an 'and_expr' by adding one more check.
|
|
||||||
"""
|
|
||||||
|
|
||||||
return [('and_expr', and_expr.add_check(check))]
|
return [('and_expr', and_expr.add_check(check))]
|
||||||
|
|
||||||
@reducer('check', 'or', 'check')
|
@reducer('check', 'or', 'check')
|
||||||
def _make_or_expr(self, check1, _or, check2):
|
def _make_or_expr(self, check1, _or, check2):
|
||||||
"""
|
"""Create an 'or_expr'.
|
||||||
Create an 'or_expr' from two checks joined by the 'or'
|
|
||||||
operator.
|
Join two checks by the 'or' operator.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return [('or_expr', OrCheck([check1, check2]))]
|
return [('or_expr', OrCheck([check1, check2]))]
|
||||||
|
|
||||||
@reducer('or_expr', 'or', 'check')
|
@reducer('or_expr', 'or', 'check')
|
||||||
def _extend_or_expr(self, or_expr, _or, check):
|
def _extend_or_expr(self, or_expr, _or, check):
|
||||||
"""
|
"""Extend an 'or_expr' by adding one more check."""
|
||||||
Extend an 'or_expr' by adding one more check.
|
|
||||||
"""
|
|
||||||
|
|
||||||
return [('or_expr', or_expr.add_check(check))]
|
return [('or_expr', or_expr.add_check(check))]
|
||||||
|
|
||||||
@ -659,7 +793,8 @@ class ParseState(object):
|
|||||||
|
|
||||||
|
|
||||||
def _parse_text_rule(rule):
|
def _parse_text_rule(rule):
|
||||||
"""
|
"""Parses policy to the tree.
|
||||||
|
|
||||||
Translates a policy written in the policy language into a tree of
|
Translates a policy written in the policy language into a tree of
|
||||||
Check objects.
|
Check objects.
|
||||||
"""
|
"""
|
||||||
@ -677,26 +812,23 @@ def _parse_text_rule(rule):
|
|||||||
return state.result
|
return state.result
|
||||||
except ValueError:
|
except ValueError:
|
||||||
# Couldn't parse the rule
|
# Couldn't parse the rule
|
||||||
LOG.exception(_("Failed to understand rule %(rule)r") % locals())
|
LOG.exception(_LE("Failed to understand rule %s") % rule)
|
||||||
|
|
||||||
# Fail closed
|
# Fail closed
|
||||||
return FalseCheck()
|
return FalseCheck()
|
||||||
|
|
||||||
|
|
||||||
def parse_rule(rule):
|
def parse_rule(rule):
|
||||||
"""
|
"""Parses a policy rule into a tree of Check objects."""
|
||||||
Parses a policy rule into a tree of Check objects.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# If the rule is a string, it's in the policy language
|
# If the rule is a string, it's in the policy language
|
||||||
if isinstance(rule, basestring):
|
if isinstance(rule, six.string_types):
|
||||||
return _parse_text_rule(rule)
|
return _parse_text_rule(rule)
|
||||||
return _parse_list_rule(rule)
|
return _parse_list_rule(rule)
|
||||||
|
|
||||||
|
|
||||||
def register(name, func=None):
|
def register(name, func=None):
|
||||||
"""
|
"""Register a function or Check class 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 check type
|
'role', etc. If name is None, a default check type
|
||||||
@ -723,13 +855,11 @@ def register(name, func=None):
|
|||||||
|
|
||||||
@register("rule")
|
@register("rule")
|
||||||
class RuleCheck(Check):
|
class RuleCheck(Check):
|
||||||
def __call__(self, target, creds):
|
def __call__(self, target, creds, enforcer):
|
||||||
"""
|
"""Recursively checks credentials based on the defined rules."""
|
||||||
Recursively checks credentials based on the defined rules.
|
|
||||||
"""
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return _rules[self.match](target, creds)
|
return enforcer.rules[self.match](target, creds, enforcer)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
# We don't have any matching rule; fail closed
|
# We don't have any matching rule; fail closed
|
||||||
return False
|
return False
|
||||||
@ -737,7 +867,7 @@ class RuleCheck(Check):
|
|||||||
|
|
||||||
@register("role")
|
@register("role")
|
||||||
class RoleCheck(Check):
|
class RoleCheck(Check):
|
||||||
def __call__(self, target, creds):
|
def __call__(self, target, creds, enforcer):
|
||||||
"""Check that there is a matching role in the cred dict."""
|
"""Check that there is a matching role in the cred dict."""
|
||||||
|
|
||||||
return self.match.lower() in [x.lower() for x in creds['roles']]
|
return self.match.lower() in [x.lower() for x in creds['roles']]
|
||||||
@ -745,9 +875,8 @@ class RoleCheck(Check):
|
|||||||
|
|
||||||
@register('http')
|
@register('http')
|
||||||
class HttpCheck(Check):
|
class HttpCheck(Check):
|
||||||
def __call__(self, target, creds):
|
def __call__(self, target, creds, enforcer):
|
||||||
"""
|
"""Check http: rules by calling to a remote server.
|
||||||
Check http: rules by calling to a remote server.
|
|
||||||
|
|
||||||
This example implementation simply verifies that the response
|
This example implementation simply verifies that the response
|
||||||
is exactly 'True'.
|
is exactly 'True'.
|
||||||
@ -756,25 +885,40 @@ class HttpCheck(Check):
|
|||||||
url = ('http:' + self.match) % target
|
url = ('http:' + self.match) % target
|
||||||
data = {'target': jsonutils.dumps(target),
|
data = {'target': jsonutils.dumps(target),
|
||||||
'credentials': jsonutils.dumps(creds)}
|
'credentials': jsonutils.dumps(creds)}
|
||||||
post_data = urllib.urlencode(data)
|
post_data = urlparse.urlencode(data)
|
||||||
f = urllib2.urlopen(url, post_data)
|
f = urlrequest.urlopen(url, post_data)
|
||||||
return f.read() == "True"
|
return f.read() == "True"
|
||||||
|
|
||||||
|
|
||||||
@register(None)
|
@register(None)
|
||||||
class GenericCheck(Check):
|
class GenericCheck(Check):
|
||||||
def __call__(self, target, creds):
|
def __call__(self, target, creds, enforcer):
|
||||||
"""
|
"""Check an individual match.
|
||||||
Check an individual match.
|
|
||||||
|
|
||||||
Matches look like:
|
Matches look like:
|
||||||
|
|
||||||
tenant:%(tenant_id)s
|
tenant:%(tenant_id)s
|
||||||
role:compute:admin
|
role:compute:admin
|
||||||
|
True:%(user.enabled)s
|
||||||
|
'Member':%(role.name)s
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# TODO(termie): do dict inspection via dot syntax
|
try:
|
||||||
match = self.match % target
|
match = self.match % target
|
||||||
if self.kind in creds:
|
except KeyError:
|
||||||
return match == six.text_type(creds[self.kind])
|
# While doing GenericCheck if key not
|
||||||
return False
|
# present in Target return false
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Try to interpret self.kind as a literal
|
||||||
|
leftval = ast.literal_eval(self.kind)
|
||||||
|
except ValueError:
|
||||||
|
try:
|
||||||
|
kind_parts = self.kind.split('.')
|
||||||
|
leftval = creds
|
||||||
|
for kind_part in kind_parts:
|
||||||
|
leftval = leftval[kind_part]
|
||||||
|
except KeyError:
|
||||||
|
return False
|
||||||
|
return match == six.text_type(leftval)
|
||||||
|
@ -36,8 +36,8 @@ from neutron.openstack.common import policy
|
|||||||
|
|
||||||
|
|
||||||
LOG = log.getLogger(__name__)
|
LOG = log.getLogger(__name__)
|
||||||
_POLICY_PATH = None
|
|
||||||
_POLICY_CACHE = {}
|
_ENFORCER = None
|
||||||
ADMIN_CTX_POLICY = 'context_is_admin'
|
ADMIN_CTX_POLICY = 'context_is_admin'
|
||||||
ADVSVC_CTX_POLICY = 'context_is_advsvc'
|
ADVSVC_CTX_POLICY = 'context_is_advsvc'
|
||||||
# Maps deprecated 'extension' policies to new-style policies
|
# Maps deprecated 'extension' policies to new-style policies
|
||||||
@ -57,28 +57,32 @@ DEPRECATED_ACTION_MAP = {
|
|||||||
'set': ['create', 'update']
|
'set': ['create', 'update']
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg.CONF.import_opt('policy_file', 'neutron.common.config')
|
|
||||||
|
|
||||||
|
|
||||||
def reset():
|
def reset():
|
||||||
global _POLICY_PATH
|
global _ENFORCER
|
||||||
global _POLICY_CACHE
|
if _ENFORCER:
|
||||||
_POLICY_PATH = None
|
_ENFORCER.clear()
|
||||||
_POLICY_CACHE = {}
|
_ENFORCER = None
|
||||||
policy.reset()
|
|
||||||
|
|
||||||
|
|
||||||
def init():
|
def init():
|
||||||
global _POLICY_PATH
|
"""Init an instance of the Enforcer class."""
|
||||||
global _POLICY_CACHE
|
|
||||||
if not _POLICY_PATH:
|
global _ENFORCER
|
||||||
_POLICY_PATH = utils.find_config_file({}, cfg.CONF.policy_file)
|
if not _ENFORCER:
|
||||||
if not _POLICY_PATH:
|
_ENFORCER = policy.Enforcer()
|
||||||
raise exceptions.PolicyFileNotFound(path=cfg.CONF.policy_file)
|
# NOTE: Method _get_policy_path in common.policy can not always locate
|
||||||
# pass _set_brain to read_cached_file so that the policy brain
|
# neutron policy file (when init() is called in tests),
|
||||||
# is reset only if the file has changed
|
# so set it explicitly.
|
||||||
utils.read_cached_file(_POLICY_PATH, _POLICY_CACHE,
|
_ENFORCER.policy_path = utils.find_config_file({},
|
||||||
reload_func=_set_rules)
|
cfg.CONF.policy_file)
|
||||||
|
_ENFORCER.load_rules(True)
|
||||||
|
|
||||||
|
|
||||||
|
def refresh():
|
||||||
|
"""Reset policy and init a new instance of Enforcer."""
|
||||||
|
reset()
|
||||||
|
init()
|
||||||
|
|
||||||
|
|
||||||
def get_resource_and_action(action):
|
def get_resource_and_action(action):
|
||||||
@ -87,12 +91,17 @@ def get_resource_and_action(action):
|
|||||||
return ("%ss" % data[-1], data[0] != 'get')
|
return ("%ss" % data[-1], data[0] != 'get')
|
||||||
|
|
||||||
|
|
||||||
def _set_rules(data):
|
def set_rules(policies, overwrite=True):
|
||||||
default_rule = 'default'
|
"""Set rules based on the provided dict of rules.
|
||||||
LOG.debug(_("Loading policies from file: %s"), _POLICY_PATH)
|
|
||||||
|
:param policies: New policies to use. It should be an instance of dict.
|
||||||
|
:param overwrite: Whether to overwrite current rules or update them
|
||||||
|
with the new rules.
|
||||||
|
"""
|
||||||
|
|
||||||
|
LOG.debug("Loading policies from file: %s", _ENFORCER.policy_path)
|
||||||
# Ensure backward compatibility with folsom/grizzly convention
|
# Ensure backward compatibility with folsom/grizzly convention
|
||||||
# for extension rules
|
# for extension rules
|
||||||
policies = policy.Rules.load_json(data, default_rule)
|
|
||||||
for pol in policies.keys():
|
for pol in policies.keys():
|
||||||
if any([pol.startswith(depr_pol) for depr_pol in
|
if any([pol.startswith(depr_pol) for depr_pol in
|
||||||
DEPRECATED_POLICY_MAP.keys()]):
|
DEPRECATED_POLICY_MAP.keys()]):
|
||||||
@ -120,7 +129,8 @@ def _set_rules(data):
|
|||||||
LOG.error(_LE("Backward compatibility unavailable for "
|
LOG.error(_LE("Backward compatibility unavailable for "
|
||||||
"deprecated policy %s. The policy will "
|
"deprecated policy %s. The policy will "
|
||||||
"not be enforced"), pol)
|
"not be enforced"), pol)
|
||||||
policy.set_rules(policies)
|
init()
|
||||||
|
_ENFORCER.set_rules(policies, overwrite)
|
||||||
|
|
||||||
|
|
||||||
def _is_attribute_explicitly_set(attribute_name, resource, target, action):
|
def _is_attribute_explicitly_set(attribute_name, resource, target, action):
|
||||||
@ -253,7 +263,7 @@ class OwnerCheck(policy.Check):
|
|||||||
reason=err_reason)
|
reason=err_reason)
|
||||||
super(OwnerCheck, self).__init__(kind, match)
|
super(OwnerCheck, self).__init__(kind, match)
|
||||||
|
|
||||||
def __call__(self, target, creds):
|
def __call__(self, target, creds, enforcer):
|
||||||
if self.target_field not in target:
|
if self.target_field not in target:
|
||||||
# policy needs a plugin check
|
# policy needs a plugin check
|
||||||
# target field is in the form resource:field
|
# target field is in the form resource:field
|
||||||
@ -337,7 +347,7 @@ class FieldCheck(policy.Check):
|
|||||||
self.field = field
|
self.field = field
|
||||||
self.value = conv_func(value)
|
self.value = conv_func(value)
|
||||||
|
|
||||||
def __call__(self, target_dict, cred_dict):
|
def __call__(self, target_dict, cred_dict, enforcer):
|
||||||
target_value = target_dict.get(self.field)
|
target_value = target_dict.get(self.field)
|
||||||
# target_value might be a boolean, explicitly compare with None
|
# target_value might be a boolean, explicitly compare with None
|
||||||
if target_value is None:
|
if target_value is None:
|
||||||
@ -376,9 +386,9 @@ def check(context, action, target, plugin=None, might_not_exist=False):
|
|||||||
|
|
||||||
:return: Returns True if access is permitted else False.
|
:return: Returns True if access is permitted else False.
|
||||||
"""
|
"""
|
||||||
if might_not_exist and not (policy._rules and action in policy._rules):
|
if might_not_exist and not (_ENFORCER.rules and action in _ENFORCER.rules):
|
||||||
return True
|
return True
|
||||||
return policy.check(*(_prepare_check(context, action, target)))
|
return _ENFORCER.enforce(*(_prepare_check(context, action, target)))
|
||||||
|
|
||||||
|
|
||||||
def enforce(context, action, target, plugin=None):
|
def enforce(context, action, target, plugin=None):
|
||||||
@ -393,14 +403,16 @@ def enforce(context, action, target, plugin=None):
|
|||||||
:param plugin: currently unused and deprecated.
|
:param plugin: currently unused and deprecated.
|
||||||
Kept for backward compatibility.
|
Kept for backward compatibility.
|
||||||
|
|
||||||
:raises neutron.exceptions.PolicyNotAuthorized: if verification fails.
|
:raises neutron.openstack.common.policy.PolicyNotAuthorized:
|
||||||
|
if verification fails.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
rule, target, credentials = _prepare_check(context, action, target)
|
rule, target, credentials = _prepare_check(context, action, target)
|
||||||
result = policy.check(rule, target, credentials, action=action)
|
try:
|
||||||
if not result:
|
result = _ENFORCER.enforce(rule, target, credentials,
|
||||||
LOG.debug(_("Failed policy check for '%s'"), action)
|
action=action, do_raise=True)
|
||||||
raise exceptions.PolicyNotAuthorized(action=action)
|
except policy.PolicyNotAuthorized:
|
||||||
|
with excutils.save_and_reraise_exception():
|
||||||
|
LOG.debug("Failed policy check for '%s'", action)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
@ -412,9 +424,9 @@ def check_is_admin(context):
|
|||||||
target = credentials
|
target = credentials
|
||||||
# Backward compatibility: if ADMIN_CTX_POLICY is not
|
# Backward compatibility: if ADMIN_CTX_POLICY is not
|
||||||
# found, default to validating role:admin
|
# found, default to validating role:admin
|
||||||
admin_policy = (ADMIN_CTX_POLICY if ADMIN_CTX_POLICY in policy._rules
|
admin_policy = (ADMIN_CTX_POLICY if ADMIN_CTX_POLICY in _ENFORCER.rules
|
||||||
else 'role:admin')
|
else 'role:admin')
|
||||||
return policy.check(admin_policy, target, credentials)
|
return _ENFORCER.enforce(admin_policy, target, credentials)
|
||||||
|
|
||||||
|
|
||||||
def check_is_advsvc(context):
|
def check_is_advsvc(context):
|
||||||
@ -425,16 +437,16 @@ def check_is_advsvc(context):
|
|||||||
target = credentials
|
target = credentials
|
||||||
# Backward compatibility: if ADVSVC_CTX_POLICY is not
|
# Backward compatibility: if ADVSVC_CTX_POLICY is not
|
||||||
# found, default to validating role:advsvc
|
# found, default to validating role:advsvc
|
||||||
advsvc_policy = (ADVSVC_CTX_POLICY in policy._rules
|
advsvc_policy = (ADVSVC_CTX_POLICY in _ENFORCER.rules
|
||||||
and ADVSVC_CTX_POLICY or 'role:advsvc')
|
and ADVSVC_CTX_POLICY or 'role:advsvc')
|
||||||
return policy.check(advsvc_policy, target, credentials)
|
return _ENFORCER.enforce(advsvc_policy, target, credentials)
|
||||||
|
|
||||||
|
|
||||||
def _extract_roles(rule, roles):
|
def _extract_roles(rule, roles):
|
||||||
if isinstance(rule, policy.RoleCheck):
|
if isinstance(rule, policy.RoleCheck):
|
||||||
roles.append(rule.match.lower())
|
roles.append(rule.match.lower())
|
||||||
elif isinstance(rule, policy.RuleCheck):
|
elif isinstance(rule, policy.RuleCheck):
|
||||||
_extract_roles(policy._rules[rule.match], roles)
|
_extract_roles(_ENFORCER.rules[rule.match], roles)
|
||||||
elif hasattr(rule, 'rules'):
|
elif hasattr(rule, 'rules'):
|
||||||
for rule in rule.rules:
|
for rule in rule.rules:
|
||||||
_extract_roles(rule, roles)
|
_extract_roles(rule, roles)
|
||||||
@ -451,10 +463,10 @@ def get_admin_roles():
|
|||||||
# plugin modules. For backward compatibility it returns the literal
|
# plugin modules. For backward compatibility it returns the literal
|
||||||
# admin if ADMIN_CTX_POLICY is not defined
|
# admin if ADMIN_CTX_POLICY is not defined
|
||||||
init()
|
init()
|
||||||
if not policy._rules or ADMIN_CTX_POLICY not in policy._rules:
|
if not _ENFORCER.rules or ADMIN_CTX_POLICY not in _ENFORCER.rules:
|
||||||
return ['admin']
|
return ['admin']
|
||||||
try:
|
try:
|
||||||
admin_ctx_rule = policy._rules[ADMIN_CTX_POLICY]
|
admin_ctx_rule = _ENFORCER.rules[ADMIN_CTX_POLICY]
|
||||||
except (KeyError, TypeError):
|
except (KeyError, TypeError):
|
||||||
return
|
return
|
||||||
roles = []
|
roles = []
|
||||||
|
@ -31,10 +31,10 @@ class TestL3Sync(test_nuage_plugin.NuagePluginV2TestCase,
|
|||||||
extraroute_test.ExtraRouteDBIntTestCase):
|
extraroute_test.ExtraRouteDBIntTestCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.session = context.get_admin_context().session
|
super(TestL3Sync, self).setUp()
|
||||||
self.syncmanager = sync.SyncManager(
|
self.syncmanager = sync.SyncManager(
|
||||||
test_nuage_plugin.getNuageClient())
|
test_nuage_plugin.getNuageClient())
|
||||||
super(TestL3Sync, self).setUp()
|
self.session = context.get_admin_context().session
|
||||||
|
|
||||||
def _make_floatingip_for_tenant_port(self, net_id, port_id, tenant_id):
|
def _make_floatingip_for_tenant_port(self, net_id, port_id, tenant_id):
|
||||||
data = {'floatingip': {'floating_network_id': net_id,
|
data = {'floatingip': {'floating_network_id': net_id,
|
||||||
@ -194,10 +194,10 @@ class TestL3Sync(test_nuage_plugin.NuagePluginV2TestCase,
|
|||||||
class TestNetPartSync(test_netpartition.NetPartitionTestCase):
|
class TestNetPartSync(test_netpartition.NetPartitionTestCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.session = context.get_admin_context().session
|
|
||||||
self.syncmanager = sync.SyncManager(
|
self.syncmanager = sync.SyncManager(
|
||||||
test_nuage_plugin.getNuageClient())
|
test_nuage_plugin.getNuageClient())
|
||||||
super(TestNetPartSync, self).setUp()
|
super(TestNetPartSync, self).setUp()
|
||||||
|
self.session = context.get_admin_context().session
|
||||||
|
|
||||||
def test_net_partition_sync(self):
|
def test_net_partition_sync(self):
|
||||||
# If the net-partition exists in neutron and not in VSD,
|
# If the net-partition exists in neutron and not in VSD,
|
||||||
@ -225,10 +225,10 @@ class TestNetPartSync(test_netpartition.NetPartitionTestCase):
|
|||||||
class TestL2Sync(test_nuage_plugin.NuagePluginV2TestCase):
|
class TestL2Sync(test_nuage_plugin.NuagePluginV2TestCase):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.session = context.get_admin_context().session
|
super(TestL2Sync, self).setUp()
|
||||||
self.syncmanager = sync.SyncManager(
|
self.syncmanager = sync.SyncManager(
|
||||||
test_nuage_plugin.getNuageClient())
|
test_nuage_plugin.getNuageClient())
|
||||||
super(TestL2Sync, self).setUp()
|
self.session = context.get_admin_context().session
|
||||||
|
|
||||||
def test_subnet_sync(self):
|
def test_subnet_sync(self):
|
||||||
# If the subnet exists in neutron and not in VSD,
|
# If the subnet exists in neutron and not in VSD,
|
||||||
@ -274,10 +274,10 @@ class TestL2Sync(test_nuage_plugin.NuagePluginV2TestCase):
|
|||||||
class TestSecurityGroupSync(test_sg.TestSecurityGroups):
|
class TestSecurityGroupSync(test_sg.TestSecurityGroups):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.session = context.get_admin_context().session
|
|
||||||
self.syncmanager = sync.SyncManager(
|
self.syncmanager = sync.SyncManager(
|
||||||
test_nuage_plugin.getNuageClient())
|
test_nuage_plugin.getNuageClient())
|
||||||
super(TestSecurityGroupSync, self).setUp()
|
super(TestSecurityGroupSync, self).setUp()
|
||||||
|
self.session = context.get_admin_context().session
|
||||||
|
|
||||||
def test_sg_get(self):
|
def test_sg_get(self):
|
||||||
with self.security_group() as sg:
|
with self.security_group() as sg:
|
||||||
|
@ -1028,14 +1028,13 @@ class JSONV2TestCase(APIv2TestBase, testlib_api.WebTestCase):
|
|||||||
tenant_id = _uuid()
|
tenant_id = _uuid()
|
||||||
# Inject rule in policy engine
|
# Inject rule in policy engine
|
||||||
policy.init()
|
policy.init()
|
||||||
common_policy._rules['get_network:name'] = common_policy.parse_rule(
|
self.addCleanup(policy.reset)
|
||||||
"rule:admin_only")
|
rules = {'get_network:name': common_policy.parse_rule(
|
||||||
|
"rule:admin_only")}
|
||||||
|
policy.set_rules(rules, overwrite=False)
|
||||||
res = self._test_get(tenant_id, tenant_id, 200)
|
res = self._test_get(tenant_id, tenant_id, 200)
|
||||||
res = self.deserialize(res)
|
res = self.deserialize(res)
|
||||||
try:
|
self.assertNotIn('name', res['network'])
|
||||||
self.assertNotIn('name', res['network'])
|
|
||||||
finally:
|
|
||||||
del common_policy._rules['get_network:name']
|
|
||||||
|
|
||||||
def _test_update(self, req_tenant_id, real_tenant_id, expected_code,
|
def _test_update(self, req_tenant_id, real_tenant_id, expected_code,
|
||||||
expect_errors=False):
|
expect_errors=False):
|
||||||
|
@ -15,11 +15,15 @@
|
|||||||
|
|
||||||
"""Test of Policy Engine For Neutron"""
|
"""Test of Policy Engine For Neutron"""
|
||||||
|
|
||||||
|
import StringIO
|
||||||
import urllib2
|
import urllib2
|
||||||
|
|
||||||
import fixtures
|
import fixtures
|
||||||
import mock
|
import mock
|
||||||
|
from oslo.config import cfg
|
||||||
|
from oslo.serialization import jsonutils
|
||||||
import six
|
import six
|
||||||
|
import six.moves.urllib.request as urlrequest
|
||||||
|
|
||||||
import neutron
|
import neutron
|
||||||
from neutron.api.v2 import attributes
|
from neutron.api.v2 import attributes
|
||||||
@ -28,7 +32,6 @@ from neutron.common import exceptions
|
|||||||
from neutron import context
|
from neutron import context
|
||||||
from neutron import manager
|
from neutron import manager
|
||||||
from neutron.openstack.common import importutils
|
from neutron.openstack.common import importutils
|
||||||
from neutron.openstack.common import jsonutils
|
|
||||||
from neutron.openstack.common import policy as common_policy
|
from neutron.openstack.common import policy as common_policy
|
||||||
from neutron import policy
|
from neutron import policy
|
||||||
from neutron.tests import base
|
from neutron.tests import base
|
||||||
@ -37,46 +40,35 @@ from neutron.tests import base
|
|||||||
class PolicyFileTestCase(base.BaseTestCase):
|
class PolicyFileTestCase(base.BaseTestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(PolicyFileTestCase, self).setUp()
|
super(PolicyFileTestCase, self).setUp()
|
||||||
policy.reset()
|
|
||||||
self.addCleanup(policy.reset)
|
self.addCleanup(policy.reset)
|
||||||
self.context = context.Context('fake', 'fake', is_admin=False)
|
self.context = context.Context('fake', 'fake', is_admin=False)
|
||||||
self.target = {'tenant_id': 'fake'}
|
self.target = {'tenant_id': 'fake'}
|
||||||
self.tempdir = self.useFixture(fixtures.TempDir())
|
self.tempdir = self.useFixture(fixtures.TempDir())
|
||||||
|
|
||||||
def test_modified_policy_reloads(self):
|
def test_modified_policy_reloads(self):
|
||||||
def fake_find_config_file(_1, _2):
|
tmpfilename = self.tempdir.join('policy')
|
||||||
return self.tempdir.join('policy')
|
action = "example:test"
|
||||||
|
with open(tmpfilename, "w") as policyfile:
|
||||||
with mock.patch.object(neutron.common.utils,
|
policyfile.write("""{"example:test": ""}""")
|
||||||
'find_config_file',
|
cfg.CONF.set_override('policy_file', tmpfilename)
|
||||||
new=fake_find_config_file):
|
policy.refresh()
|
||||||
tmpfilename = fake_find_config_file(None, None)
|
policy.enforce(self.context, action, self.target)
|
||||||
action = "example:test"
|
with open(tmpfilename, "w") as policyfile:
|
||||||
with open(tmpfilename, "w") as policyfile:
|
policyfile.write("""{"example:test": "!"}""")
|
||||||
policyfile.write("""{"example:test": ""}""")
|
policy.refresh()
|
||||||
policy.init()
|
self.target = {'tenant_id': 'fake_tenant'}
|
||||||
policy.enforce(self.context, action, self.target)
|
self.assertRaises(common_policy.PolicyNotAuthorized,
|
||||||
with open(tmpfilename, "w") as policyfile:
|
policy.enforce,
|
||||||
policyfile.write("""{"example:test": "!"}""")
|
self.context,
|
||||||
# NOTE(vish): reset stored policy cache so we don't have to
|
action,
|
||||||
# sleep(1)
|
self.target)
|
||||||
policy._POLICY_CACHE = {}
|
|
||||||
policy.init()
|
|
||||||
self.target = {'tenant_id': 'fake_tenant'}
|
|
||||||
self.assertRaises(exceptions.PolicyNotAuthorized,
|
|
||||||
policy.enforce,
|
|
||||||
self.context,
|
|
||||||
action,
|
|
||||||
self.target)
|
|
||||||
|
|
||||||
|
|
||||||
class PolicyTestCase(base.BaseTestCase):
|
class PolicyTestCase(base.BaseTestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(PolicyTestCase, self).setUp()
|
super(PolicyTestCase, self).setUp()
|
||||||
policy.reset()
|
|
||||||
self.addCleanup(policy.reset)
|
self.addCleanup(policy.reset)
|
||||||
# NOTE(vish): preload rules to circumvent reloading from file
|
# NOTE(vish): preload rules to circumvent reloading from file
|
||||||
policy.init()
|
|
||||||
rules = {
|
rules = {
|
||||||
"true": '@',
|
"true": '@',
|
||||||
"example:allowed": '@',
|
"example:allowed": '@',
|
||||||
@ -88,21 +80,21 @@ class PolicyTestCase(base.BaseTestCase):
|
|||||||
"example:lowercase_admin": "role:admin or role:sysadmin",
|
"example:lowercase_admin": "role:admin or role:sysadmin",
|
||||||
"example:uppercase_admin": "role:ADMIN or role:sysadmin",
|
"example:uppercase_admin": "role:ADMIN or role:sysadmin",
|
||||||
}
|
}
|
||||||
|
policy.refresh()
|
||||||
# NOTE(vish): then overload underlying rules
|
# NOTE(vish): then overload underlying rules
|
||||||
common_policy.set_rules(common_policy.Rules(
|
policy.set_rules(dict((k, common_policy.parse_rule(v))
|
||||||
dict((k, common_policy.parse_rule(v))
|
for k, v in rules.items()))
|
||||||
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 = {}
|
||||||
|
|
||||||
def test_enforce_nonexistent_action_throws(self):
|
def test_enforce_nonexistent_action_throws(self):
|
||||||
action = "example:noexist"
|
action = "example:noexist"
|
||||||
self.assertRaises(exceptions.PolicyNotAuthorized, policy.enforce,
|
self.assertRaises(common_policy.PolicyNotAuthorized, policy.enforce,
|
||||||
self.context, action, self.target)
|
self.context, action, self.target)
|
||||||
|
|
||||||
def test_enforce_bad_action_throws(self):
|
def test_enforce_bad_action_throws(self):
|
||||||
action = "example:denied"
|
action = "example:denied"
|
||||||
self.assertRaises(exceptions.PolicyNotAuthorized, policy.enforce,
|
self.assertRaises(common_policy.PolicyNotAuthorized, policy.enforce,
|
||||||
self.context, action, self.target)
|
self.context, action, self.target)
|
||||||
|
|
||||||
def test_check_bad_action_noraise(self):
|
def test_check_bad_action_noraise(self):
|
||||||
@ -123,16 +115,13 @@ class PolicyTestCase(base.BaseTestCase):
|
|||||||
result = policy.enforce(self.context, action, self.target)
|
result = policy.enforce(self.context, action, self.target)
|
||||||
self.assertEqual(result, True)
|
self.assertEqual(result, True)
|
||||||
|
|
||||||
def test_enforce_http_true(self):
|
@mock.patch.object(urlrequest, 'urlopen',
|
||||||
|
return_value=StringIO.StringIO("True"))
|
||||||
def fakeurlopen(url, post_data):
|
def test_enforce_http_true(self, mock_urlrequest):
|
||||||
return six.StringIO("True")
|
action = "example:get_http"
|
||||||
|
target = {}
|
||||||
with mock.patch.object(urllib2, 'urlopen', new=fakeurlopen):
|
result = policy.enforce(self.context, action, target)
|
||||||
action = "example:get_http"
|
self.assertEqual(result, True)
|
||||||
target = {}
|
|
||||||
result = policy.enforce(self.context, action, target)
|
|
||||||
self.assertEqual(result, True)
|
|
||||||
|
|
||||||
def test_enforce_http_false(self):
|
def test_enforce_http_false(self):
|
||||||
|
|
||||||
@ -142,20 +131,21 @@ class PolicyTestCase(base.BaseTestCase):
|
|||||||
with mock.patch.object(urllib2, 'urlopen', new=fakeurlopen):
|
with mock.patch.object(urllib2, 'urlopen', new=fakeurlopen):
|
||||||
action = "example:get_http"
|
action = "example:get_http"
|
||||||
target = {}
|
target = {}
|
||||||
self.assertRaises(exceptions.PolicyNotAuthorized, policy.enforce,
|
self.assertRaises(common_policy.PolicyNotAuthorized,
|
||||||
self.context, action, target)
|
policy.enforce, self.context,
|
||||||
|
action, target)
|
||||||
|
|
||||||
def test_templatized_enforcement(self):
|
def test_templatized_enforcement(self):
|
||||||
target_mine = {'tenant_id': 'fake'}
|
target_mine = {'tenant_id': 'fake'}
|
||||||
target_not_mine = {'tenant_id': 'another'}
|
target_not_mine = {'tenant_id': 'another'}
|
||||||
action = "example:my_file"
|
action = "example:my_file"
|
||||||
policy.enforce(self.context, action, target_mine)
|
policy.enforce(self.context, action, target_mine)
|
||||||
self.assertRaises(exceptions.PolicyNotAuthorized, policy.enforce,
|
self.assertRaises(common_policy.PolicyNotAuthorized, policy.enforce,
|
||||||
self.context, action, target_not_mine)
|
self.context, action, target_not_mine)
|
||||||
|
|
||||||
def test_early_AND_enforcement(self):
|
def test_early_AND_enforcement(self):
|
||||||
action = "example:early_and_fail"
|
action = "example:early_and_fail"
|
||||||
self.assertRaises(exceptions.PolicyNotAuthorized, policy.enforce,
|
self.assertRaises(common_policy.PolicyNotAuthorized, policy.enforce,
|
||||||
self.context, action, self.target)
|
self.context, action, self.target)
|
||||||
|
|
||||||
def test_early_OR_enforcement(self):
|
def test_early_OR_enforcement(self):
|
||||||
@ -176,37 +166,27 @@ class DefaultPolicyTestCase(base.BaseTestCase):
|
|||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(DefaultPolicyTestCase, self).setUp()
|
super(DefaultPolicyTestCase, self).setUp()
|
||||||
policy.reset()
|
self.tempdir = self.useFixture(fixtures.TempDir())
|
||||||
policy.init()
|
tmpfilename = self.tempdir.join('policy.json')
|
||||||
self.addCleanup(policy.reset)
|
|
||||||
|
|
||||||
self.rules = {
|
self.rules = {
|
||||||
"default": '',
|
"default": '',
|
||||||
"example:exist": '!',
|
"example:exist": '!',
|
||||||
}
|
}
|
||||||
|
with open(tmpfilename, "w") as policyfile:
|
||||||
self._set_rules('default')
|
jsonutils.dump(self.rules, policyfile)
|
||||||
|
cfg.CONF.set_override('policy_file', tmpfilename)
|
||||||
|
policy.refresh()
|
||||||
|
self.addCleanup(policy.reset)
|
||||||
|
|
||||||
self.context = context.Context('fake', 'fake')
|
self.context = context.Context('fake', 'fake')
|
||||||
|
|
||||||
def _set_rules(self, default_rule):
|
|
||||||
rules = common_policy.Rules(
|
|
||||||
dict((k, common_policy.parse_rule(v))
|
|
||||||
for k, v in self.rules.items()), default_rule)
|
|
||||||
common_policy.set_rules(rules)
|
|
||||||
|
|
||||||
def test_policy_called(self):
|
def test_policy_called(self):
|
||||||
self.assertRaises(exceptions.PolicyNotAuthorized, policy.enforce,
|
self.assertRaises(common_policy.PolicyNotAuthorized, policy.enforce,
|
||||||
self.context, "example:exist", {})
|
self.context, "example:exist", {})
|
||||||
|
|
||||||
def test_not_found_policy_calls_default(self):
|
def test_not_found_policy_calls_default(self):
|
||||||
policy.enforce(self.context, "example:noexist", {})
|
policy.enforce(self.context, "example:noexist", {})
|
||||||
|
|
||||||
def test_default_not_found(self):
|
|
||||||
self._set_rules("default_noexist")
|
|
||||||
self.assertRaises(exceptions.PolicyNotAuthorized, policy.enforce,
|
|
||||||
self.context, "example:noexist", {})
|
|
||||||
|
|
||||||
|
|
||||||
FAKE_RESOURCE_NAME = 'something'
|
FAKE_RESOURCE_NAME = 'something'
|
||||||
FAKE_RESOURCE = {"%ss" % FAKE_RESOURCE_NAME:
|
FAKE_RESOURCE = {"%ss" % FAKE_RESOURCE_NAME:
|
||||||
@ -225,8 +205,7 @@ class NeutronPolicyTestCase(base.BaseTestCase):
|
|||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(NeutronPolicyTestCase, self).setUp()
|
super(NeutronPolicyTestCase, self).setUp()
|
||||||
policy.reset()
|
policy.refresh()
|
||||||
policy.init()
|
|
||||||
self.addCleanup(policy.reset)
|
self.addCleanup(policy.reset)
|
||||||
self.admin_only_legacy = "role:admin"
|
self.admin_only_legacy = "role:admin"
|
||||||
self.admin_or_owner_legacy = "role:admin or tenant_id:%(tenant_id)s"
|
self.admin_or_owner_legacy = "role:admin or tenant_id:%(tenant_id)s"
|
||||||
@ -251,6 +230,7 @@ class NeutronPolicyTestCase(base.BaseTestCase):
|
|||||||
"update_network:shared": "rule:admin_only",
|
"update_network:shared": "rule:admin_only",
|
||||||
"get_network": "rule:admin_or_owner or rule:shared or "
|
"get_network": "rule:admin_or_owner or rule:shared or "
|
||||||
"rule:external or rule:context_is_advsvc",
|
"rule:external or rule:context_is_advsvc",
|
||||||
|
"create_subnet": "rule:admin_or_network_owner",
|
||||||
"create_port:mac": "rule:admin_or_network_owner or "
|
"create_port:mac": "rule:admin_or_network_owner or "
|
||||||
"rule:context_is_advsvc",
|
"rule:context_is_advsvc",
|
||||||
"update_port": "rule:admin_or_owner or rule:context_is_advsvc",
|
"update_port": "rule:admin_or_owner or rule:context_is_advsvc",
|
||||||
@ -267,8 +247,9 @@ class NeutronPolicyTestCase(base.BaseTestCase):
|
|||||||
"rule:shared"
|
"rule:shared"
|
||||||
}.items())
|
}.items())
|
||||||
|
|
||||||
def fakepolicyinit():
|
def fakepolicyinit(**kwargs):
|
||||||
common_policy.set_rules(common_policy.Rules(self.rules))
|
enf = policy._ENFORCER
|
||||||
|
enf.set_rules(common_policy.Rules(self.rules))
|
||||||
|
|
||||||
def remove_fake_resource():
|
def remove_fake_resource():
|
||||||
del attributes.RESOURCE_ATTRIBUTE_MAP["%ss" % FAKE_RESOURCE_NAME]
|
del attributes.RESOURCE_ATTRIBUTE_MAP["%ss" % FAKE_RESOURCE_NAME]
|
||||||
@ -314,22 +295,22 @@ class NeutronPolicyTestCase(base.BaseTestCase):
|
|||||||
|
|
||||||
def test_nonadmin_write_on_private_fails(self):
|
def test_nonadmin_write_on_private_fails(self):
|
||||||
self._test_nonadmin_action_on_attr('create', 'shared', False,
|
self._test_nonadmin_action_on_attr('create', 'shared', False,
|
||||||
exceptions.PolicyNotAuthorized)
|
common_policy.PolicyNotAuthorized)
|
||||||
|
|
||||||
def test_nonadmin_read_on_private_fails(self):
|
def test_nonadmin_read_on_private_fails(self):
|
||||||
self._test_nonadmin_action_on_attr('get', 'shared', False,
|
self._test_nonadmin_action_on_attr('get', 'shared', False,
|
||||||
exceptions.PolicyNotAuthorized)
|
common_policy.PolicyNotAuthorized)
|
||||||
|
|
||||||
def test_nonadmin_write_on_shared_fails(self):
|
def test_nonadmin_write_on_shared_fails(self):
|
||||||
self._test_nonadmin_action_on_attr('create', 'shared', True,
|
self._test_nonadmin_action_on_attr('create', 'shared', True,
|
||||||
exceptions.PolicyNotAuthorized)
|
common_policy.PolicyNotAuthorized)
|
||||||
|
|
||||||
def test_advsvc_get_network_works(self):
|
def test_advsvc_get_network_works(self):
|
||||||
self._test_advsvc_action_on_attr('get', 'network', 'shared', False)
|
self._test_advsvc_action_on_attr('get', 'network', 'shared', False)
|
||||||
|
|
||||||
def test_advsvc_create_network_fails(self):
|
def test_advsvc_create_network_fails(self):
|
||||||
self._test_advsvc_action_on_attr('create', 'network', 'shared', False,
|
self._test_advsvc_action_on_attr('create', 'network', 'shared', False,
|
||||||
exceptions.PolicyNotAuthorized)
|
common_policy.PolicyNotAuthorized)
|
||||||
|
|
||||||
def test_advsvc_create_port_works(self):
|
def test_advsvc_create_port_works(self):
|
||||||
self._test_advsvc_action_on_attr('create', 'port:mac', 'shared', False)
|
self._test_advsvc_action_on_attr('create', 'port:mac', 'shared', False)
|
||||||
@ -347,7 +328,7 @@ class NeutronPolicyTestCase(base.BaseTestCase):
|
|||||||
|
|
||||||
def test_advsvc_create_subnet_fails(self):
|
def test_advsvc_create_subnet_fails(self):
|
||||||
self._test_advsvc_action_on_attr('create', 'subnet', 'shared', False,
|
self._test_advsvc_action_on_attr('create', 'subnet', 'shared', False,
|
||||||
exceptions.PolicyNotAuthorized)
|
common_policy.PolicyNotAuthorized)
|
||||||
|
|
||||||
def test_nonadmin_read_on_shared_succeeds(self):
|
def test_nonadmin_read_on_shared_succeeds(self):
|
||||||
self._test_nonadmin_action_on_attr('get', 'shared', True)
|
self._test_nonadmin_action_on_attr('get', 'shared', True)
|
||||||
@ -370,7 +351,7 @@ class NeutronPolicyTestCase(base.BaseTestCase):
|
|||||||
def test_reset_adminonly_attr_to_default_fails(self):
|
def test_reset_adminonly_attr_to_default_fails(self):
|
||||||
kwargs = {const.ATTRIBUTES_TO_UPDATE: ['shared']}
|
kwargs = {const.ATTRIBUTES_TO_UPDATE: ['shared']}
|
||||||
self._test_nonadmin_action_on_attr('update', 'shared', False,
|
self._test_nonadmin_action_on_attr('update', 'shared', False,
|
||||||
exceptions.PolicyNotAuthorized,
|
common_policy.PolicyNotAuthorized,
|
||||||
**kwargs)
|
**kwargs)
|
||||||
|
|
||||||
def test_enforce_adminonly_attribute_no_context_is_admin_policy(self):
|
def test_enforce_adminonly_attribute_no_context_is_admin_policy(self):
|
||||||
@ -384,7 +365,7 @@ class NeutronPolicyTestCase(base.BaseTestCase):
|
|||||||
def test_enforce_adminonly_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(common_policy.PolicyNotAuthorized, policy.enforce,
|
||||||
self.context, action, target)
|
self.context, action, target)
|
||||||
|
|
||||||
def test_enforce_adminonly_nonadminctx_no_ctx_is_admin_policy_403(self):
|
def test_enforce_adminonly_nonadminctx_no_ctx_is_admin_policy_403(self):
|
||||||
@ -395,7 +376,7 @@ class NeutronPolicyTestCase(base.BaseTestCase):
|
|||||||
self.admin_or_owner_legacy)
|
self.admin_or_owner_legacy)
|
||||||
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(common_policy.PolicyNotAuthorized, policy.enforce,
|
||||||
self.context, action, target)
|
self.context, action, target)
|
||||||
|
|
||||||
def _test_build_subattribute_match_rule(self, validate_value):
|
def _test_build_subattribute_match_rule(self, validate_value):
|
||||||
@ -436,7 +417,7 @@ class NeutronPolicyTestCase(base.BaseTestCase):
|
|||||||
action = "create_something"
|
action = "create_something"
|
||||||
target = {'tenant_id': 'fake', 'attr': {'sub_attr_1': 'x',
|
target = {'tenant_id': 'fake', 'attr': {'sub_attr_1': 'x',
|
||||||
'sub_attr_2': 'y'}}
|
'sub_attr_2': 'y'}}
|
||||||
self.assertRaises(exceptions.PolicyNotAuthorized, policy.enforce,
|
self.assertRaises(common_policy.PolicyNotAuthorized, policy.enforce,
|
||||||
self.context, action, target, None)
|
self.context, action, target, None)
|
||||||
|
|
||||||
def test_enforce_regularuser_on_read(self):
|
def test_enforce_regularuser_on_read(self):
|
||||||
@ -536,7 +517,7 @@ class NeutronPolicyTestCase(base.BaseTestCase):
|
|||||||
rules = dict((k, common_policy.parse_rule(v)) for k, v in {
|
rules = dict((k, common_policy.parse_rule(v)) for k, v in {
|
||||||
"some_other_rule": "role:admin",
|
"some_other_rule": "role:admin",
|
||||||
}.items())
|
}.items())
|
||||||
common_policy.set_rules(common_policy.Rules(rules))
|
policy.set_rules(common_policy.Rules(rules))
|
||||||
# 'admin' role is expected for bw compatibility
|
# 'admin' role is expected for bw compatibility
|
||||||
self.assertEqual(['admin'], policy.get_admin_roles())
|
self.assertEqual(['admin'], policy.get_admin_roles())
|
||||||
|
|
||||||
@ -544,7 +525,7 @@ class NeutronPolicyTestCase(base.BaseTestCase):
|
|||||||
rules = dict((k, common_policy.parse_rule(v)) for k, v in {
|
rules = dict((k, common_policy.parse_rule(v)) for k, v in {
|
||||||
policy.ADMIN_CTX_POLICY: "role:admin",
|
policy.ADMIN_CTX_POLICY: "role:admin",
|
||||||
}.items())
|
}.items())
|
||||||
common_policy.set_rules(common_policy.Rules(rules))
|
policy.set_rules(common_policy.Rules(rules))
|
||||||
self.assertEqual(['admin'], policy.get_admin_roles())
|
self.assertEqual(['admin'], policy.get_admin_roles())
|
||||||
|
|
||||||
def test_get_roles_with_rule_check(self):
|
def test_get_roles_with_rule_check(self):
|
||||||
@ -552,7 +533,7 @@ class NeutronPolicyTestCase(base.BaseTestCase):
|
|||||||
policy.ADMIN_CTX_POLICY: "rule:some_other_rule",
|
policy.ADMIN_CTX_POLICY: "rule:some_other_rule",
|
||||||
"some_other_rule": "role:admin",
|
"some_other_rule": "role:admin",
|
||||||
}.items())
|
}.items())
|
||||||
common_policy.set_rules(common_policy.Rules(rules))
|
policy.set_rules(common_policy.Rules(rules))
|
||||||
self.assertEqual(['admin'], policy.get_admin_roles())
|
self.assertEqual(['admin'], policy.get_admin_roles())
|
||||||
|
|
||||||
def test_get_roles_with_or_check(self):
|
def test_get_roles_with_or_check(self):
|
||||||
@ -572,15 +553,15 @@ class NeutronPolicyTestCase(base.BaseTestCase):
|
|||||||
|
|
||||||
def _test_set_rules_with_deprecated_policy(self, input_rules,
|
def _test_set_rules_with_deprecated_policy(self, input_rules,
|
||||||
expected_rules):
|
expected_rules):
|
||||||
policy._set_rules(jsonutils.dumps(input_rules))
|
policy.set_rules(input_rules.copy())
|
||||||
# verify deprecated policy has been removed
|
# verify deprecated policy has been removed
|
||||||
for pol in input_rules.keys():
|
for pol in input_rules.keys():
|
||||||
self.assertNotIn(pol, common_policy._rules)
|
self.assertNotIn(pol, policy._ENFORCER.rules)
|
||||||
# verify deprecated policy was correctly translated. Iterate
|
# verify deprecated policy was correctly translated. Iterate
|
||||||
# over items for compatibility with unittest2 in python 2.6
|
# over items for compatibility with unittest2 in python 2.6
|
||||||
for rule in expected_rules:
|
for rule in expected_rules:
|
||||||
self.assertIn(rule, common_policy._rules)
|
self.assertIn(rule, policy._ENFORCER.rules)
|
||||||
self.assertEqual(str(common_policy._rules[rule]),
|
self.assertEqual(str(policy._ENFORCER.rules[rule]),
|
||||||
expected_rules[rule])
|
expected_rules[rule])
|
||||||
|
|
||||||
def test_set_rules_with_deprecated_view_policy(self):
|
def test_set_rules_with_deprecated_view_policy(self):
|
||||||
|
Loading…
Reference in New Issue
Block a user