diff --git a/etc/policy.json b/etc/policy.json index 9e373abc4f..30ef83cfe8 100644 --- a/etc/policy.json +++ b/etc/policy.json @@ -1,4 +1,4 @@ { - "default": [], - "manage_image_cache": [["role:admin"]] + "default": "", + "manage_image_cache": "role:admin" } diff --git a/glance/api/policy.py b/glance/api/policy.py index 9ec2b2acf3..cb044a2879 100644 --- a/glance/api/policy.py +++ b/glance/api/policy.py @@ -37,8 +37,8 @@ CONF.register_opts(policy_opts) DEFAULT_RULES = { - 'default': [[]], - 'manage_image_cache': [['role:admin']] + 'default': policy.TrueCheck(), + 'manage_image_cache': policy.RoleCheck('role', 'admin'), } @@ -52,18 +52,23 @@ class Enforcer(object): self.policy_file_contents = None def set_rules(self, rules): - """Create a new Brain based on the provided dict of rules""" - brain = policy.Brain(rules, self.default_rule) - policy.set_brain(brain) + """Create a new Rules object based on the provided dict of rules""" + rules_obj = policy.Rules(rules, self.default_rule) + policy.set_rules(rules_obj) def load_rules(self): """Set the rules found in the json file on disk""" if self.policy_path: rules = self._read_policy_file() - LOG.debug(_('Loaded policy rules: %s') % rules) + rule_type = "" else: rules = DEFAULT_RULES - LOG.debug(_('Using default policy rules: %s') % rules) + rule_type = "default " + + text_rules = dict((k, str(v)) for k, v in rules.items()) + LOG.debug(_('Loaded %(rule_type)spolicy rules: %(text_rules)s') % + locals()) + self.set_rules(rules) @staticmethod @@ -86,10 +91,32 @@ class Enforcer(object): LOG.debug(_("Loading policy from %s") % self.policy_path) with open(self.policy_path) as fap: raw_contents = fap.read() - self.policy_file_contents = json.loads(raw_contents) + rules_dict = json.loads(raw_contents) + self.policy_file_contents = dict( + (k, policy.parse_rule(v)) + for k, v in rules_dict.items()) self.policy_file_mtime = mtime return self.policy_file_contents + def _check(self, context, rule, target, *args, **kwargs): + """Verifies that the action is valid on the target in this context. + + :param context: Glance request context + :param rule: String representing the action to be checked + :param object: Dictionary representing the object of the action. + :raises: `glance.common.exception.Forbidden` + :returns: A non-False value if access is allowed. + """ + self.load_rules() + + credentials = { + 'roles': context.roles, + 'user': context.user, + 'tenant': context.tenant, + } + + return policy.check(rule, target, credentials, *args, **kwargs) + def enforce(self, context, action, target): """Verifies that the action is valid on the target in this context. @@ -97,16 +124,17 @@ class Enforcer(object): :param action: String representing the action to be checked :param object: Dictionary representing the object of the action. :raises: `glance.common.exception.Forbidden` - :returns: None + :returns: A non-False value if access is allowed. """ - self.load_rules() + return self._check(context, action, target, + exception.Forbidden, action=action) - match_list = ('rule:%s' % action,) - credentials = { - 'roles': context.roles, - 'user': context.user, - 'tenant': context.tenant, - } + def check(self, context, action, target): + """Verifies that the action is valid on the target in this context. - policy.enforce(match_list, target, credentials, - exception.Forbidden, action=action) + :param context: Glance request context + :param action: String representing the action to be checked + :param object: Dictionary representing the object of the action. + :returns: A non-False value if access is allowed. + """ + return self._check(context, action, target) diff --git a/glance/openstack/common/policy.py b/glance/openstack/common/policy.py index 3026b2549a..676ac2667d 100644 --- a/glance/openstack/common/policy.py +++ b/glance/openstack/common/policy.py @@ -1,6 +1,6 @@ # vim: tabstop=4 shiftwidth=4 softtabstop=4 -# Copyright (c) 2011 OpenStack, LLC. +# Copyright (c) 2012 OpenStack, LLC. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -15,10 +15,52 @@ # License for the specific language governing permissions and limitations # 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 re import urllib + import urllib2 from glance.openstack.common.gettextutils import _ @@ -28,217 +70,650 @@ from glance.openstack.common import jsonutils LOG = logging.getLogger(__name__) -_BRAIN = None +_rules = None +_checks = {} -def set_brain(brain): - """Set the brain used by enforce(). - - Defaults use Brain() if not set. - +class Rules(dict): """ - global _BRAIN - _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 + A store for rules. Handles the default_rule setting directly. """ - 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 def load_json(cls, data, default_rule=None): - """Init a brain using json instead of a rules dictionary.""" - rules_dict = jsonutils.loads(data) - return cls(rules=rules_dict, default_rule=default_rule) + """ + Allow loading of JSON rule data. + """ + + # 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): - if self.__class__ != Brain: - LOG.warning(_("Inheritance-based rules are deprecated; use " - "the default brain instead of %s.") % - self.__class__.__name__) + """Initialize the Rules store.""" - self.rules = rules or {} + super(Rules, self).__init__(rules or {}) self.default_rule = default_rule - def add_rule(self, key, match): - self.rules[key] = match + def __missing__(self, key): + """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: - match_kind, match_value = match.split(':', 1) - except Exception: - LOG.exception(_("Failed to understand rule %(match)r") % locals()) - # If the rule is invalid, fail closed - return False + # Evaluate the rule + result = _rules[rule](target, creds) + except KeyError: + # If the rule doesn't exist, fail closed + result = False - func = None - try: - old_func = getattr(self, '_check_%s' % match_kind) - except AttributeError: - func = self._checks.get(match_kind, self._checks.get(None, None)) - else: - LOG.warning(_("Inheritance-based rules are deprecated; update " - "_check_%s") % match_kind) - func = (lambda brain, kind, value, target, cred: - old_func(value, target, cred)) + # If it is False, raise the exception if requested + if exc and result is False: + raise exc(*args, **kwargs) - if not func: - LOG.error(_("No handler for matches of kind %s") % match_kind) - # Fail closed - return False + return result - return func(self, match_kind, match_value, target_dict, cred_dict) - def check(self, match_list, target_dict, cred_dict): - """Checks authorization of some rules against credentials. +class BaseCheck(object): + """ + Abstract base class for Check classes. + """ - Detailed description of the check with examples in policy.enforce(). - - :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 + __metaclass__ = abc.ABCMeta + @abc.abstractmethod + def __str__(self): """ - if not match_list: - return True - for and_list in match_list: - if isinstance(and_list, basestring): - and_list = (and_list,) - if all([self._check(item, target_dict, cred_dict) - for item in and_list]): - return True + Retrieve a string representation of the Check tree rooted at + this node. + """ + + pass + + @abc.abstractmethod + def __call__(self, target, cred): + """ + Perform the check. Returns False to reject the access or a + true value (not necessary True) to accept the access. + """ + + pass + + +class FalseCheck(BaseCheck): + """ + A policy check that always returns False (disallow). + """ + + def __str__(self): + """Return a string representation of this check.""" + + return "!" + + def __call__(self, target, cred): + """Check the policy.""" + return False -class HttpBrain(Brain): - """A brain that can check external urls for policy. - - Posts json blobs for target and credentials. - - Note that this brain is deprecated; the http check is registered - by default. +class TrueCheck(BaseCheck): + """ + A policy check that always returns True (allow). """ - 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): """ - Register a function as a policy check. + Register a function or Check class as a policy check. :param name: Gives the name of the check type, e.g., 'rule', - 'role', etc. If name is None, a default function + 'role', etc. If name is None, a default check type will be registered. - :param func: If given, provides the function to register. If not - given, returns a function taking one argument to - specify the function to register, allowing use as a - decorator. + :param func: If given, provides the function or class to register. + If not given, returns a function taking one argument + to specify the function or class to register, + allowing use as a decorator. """ - # Perform the actual decoration by registering the function. - # Returns the function for compliance with the decorator - # interface. + # Perform the actual decoration by registering the function or + # class. Returns the function or class for compliance with the + # decorator interface. def decorator(func): - # Register the function - Brain._register(name, func) + _checks[name] = func return func - # If the function is given, do the registration + # If the function or class is given, do the registration if func: return decorator(func) @@ -246,55 +721,59 @@ def register(name, func=None): @register("rule") -def _check_rule(brain, match_kind, match, target_dict, cred_dict): - """Recursively checks credentials based on the brains rules.""" - try: - new_match_list = brain.rules[match] - except KeyError: - if brain.default_rule and match != brain.default_rule: - new_match_list = ('rule:%s' % brain.default_rule,) - else: - return False +class RuleCheck(Check): + def __call__(self, target, creds): + """ + Recursively checks credentials based on the defined rules. + """ - return brain.check(new_match_list, target_dict, cred_dict) + try: + return _rules[self.match](target, creds) + except KeyError: + # We don't have any matching rule; fail closed + return False @register("role") -def _check_role(brain, match_kind, match, target_dict, cred_dict): - """Check that there is a matching role in the cred dict.""" - return match.lower() in [x.lower() for x in cred_dict['roles']] +class RoleCheck(Check): + def __call__(self, target, creds): + """Check that there is a matching role in the cred dict.""" + + return self.match.lower() in [x.lower() for x in creds['roles']] @register('http') -def _check_http(brain, match_kind, match, target_dict, cred_dict): - """Check http: rules by calling to a remote server. +class HttpCheck(Check): + def __call__(self, target, creds): + """ + Check http: rules by calling to a remote server. - This example implementation simply verifies that the response is - exactly 'True'. A custom brain using response codes could easily - be implemented. + This example implementation simply verifies that the response + is exactly 'True'. + """ - """ - url = 'http:' + (match % target_dict) - data = {'target': jsonutils.dumps(target_dict), - 'credentials': jsonutils.dumps(cred_dict)} - post_data = urllib.urlencode(data) - f = urllib2.urlopen(url, post_data) - return f.read() == "True" + url = ('http:' + self.match) % target + data = {'target': jsonutils.dumps(target), + 'credentials': jsonutils.dumps(creds)} + post_data = urllib.urlencode(data) + f = urllib2.urlopen(url, post_data) + return f.read() == "True" @register(None) -def _check_generic(brain, match_kind, match, target_dict, cred_dict): - """Check an individual match. +class GenericCheck(Check): + def __call__(self, target, creds): + """ + Check an individual match. - Matches look like: + Matches look like: - tenant:%(tenant_id)s - role:compute:admin + tenant:%(tenant_id)s + role:compute:admin + """ - """ - - # TODO(termie): do dict inspection via dot syntax - match = match % target_dict - if match_kind in cred_dict: - return match == unicode(cred_dict[match_kind]) - return False + # TODO(termie): do dict inspection via dot syntax + match = self.match % target + if self.kind in creds: + return match == unicode(creds[self.kind]) + return False diff --git a/glance/tests/etc/policy.json b/glance/tests/etc/policy.json index 6bb5967d80..5c10e078b4 100644 --- a/glance/tests/etc/policy.json +++ b/glance/tests/etc/policy.json @@ -1,3 +1,3 @@ { - "default": [] + "default": "" } diff --git a/glance/tests/functional/test_cache_middleware.py b/glance/tests/functional/test_cache_middleware.py index 2dcf7a852c..c48493e2a9 100644 --- a/glance/tests/functional/test_cache_middleware.py +++ b/glance/tests/functional/test_cache_middleware.py @@ -305,7 +305,7 @@ class BaseCacheManageMiddlewareTest(object): self.assertEqual(image_id1, cached_images[0]['image_id']) # Set policy to disallow access to cache management - rules = {"manage_image_cache": [["false:false"]]} + rules = {"manage_image_cache": '!'} self.set_policy_rules(rules) # Verify an unprivileged user cannot see cached images diff --git a/glance/tests/unit/test_policy.py b/glance/tests/unit/test_policy.py index 08833c4a8a..f801f9d7cf 100644 --- a/glance/tests/unit/test_policy.py +++ b/glance/tests/unit/test_policy.py @@ -30,7 +30,7 @@ class TestPolicyEnforcer(base.IsolatedUnitTest): enforcer.enforce(context, 'get_image', {}) def test_policy_file_custom_rules_default_location(self): - rules = {"get_image": [["false:false"]]} + rules = {"get_image": '!'} self.set_policy_rules(rules) enforcer = glance.api.policy.Enforcer() @@ -42,7 +42,7 @@ class TestPolicyEnforcer(base.IsolatedUnitTest): def test_policy_file_custom_location(self): self.config(policy_file=os.path.join(self.test_dir, 'gobble.gobble')) - rules = {"get_image": [["false:false"]]} + rules = {"get_image": '!'} self.set_policy_rules(rules) enforcer = glance.api.policy.Enforcer() @@ -51,6 +51,17 @@ class TestPolicyEnforcer(base.IsolatedUnitTest): self.assertRaises(exception.Forbidden, enforcer.enforce, context, 'get_image', {}) + def test_policy_file_check(self): + self.config(policy_file=os.path.join(self.test_dir, 'gobble.gobble')) + + rules = {"get_image": '!'} + self.set_policy_rules(rules) + + enforcer = glance.api.policy.Enforcer() + + context = glance.context.RequestContext(roles=[]) + self.assertEqual(enforcer.check(context, 'get_image', {}), False) + class TestPolicyEnforcerNoFile(test_utils.BaseTestCase): def test_policy_file_specified_but_not_found(self): diff --git a/glance/tests/unit/v1/test_api.py b/glance/tests/unit/v1/test_api.py index bc4d8f3cf1..691d42d65d 100644 --- a/glance/tests/unit/v1/test_api.py +++ b/glance/tests/unit/v1/test_api.py @@ -2238,7 +2238,7 @@ class TestGlanceAPI(base.IsolatedUnitTest): self.assertEquals(res.status_int, httplib.BAD_REQUEST) def test_add_image_unauthorized(self): - rules = {"add_image": [["false:false"]]} + rules = {"add_image": '!'} self.set_policy_rules(rules) fixture_headers = {'x-image-meta-store': 'file', 'x-image-meta-disk-format': 'vhd', @@ -2256,7 +2256,7 @@ class TestGlanceAPI(base.IsolatedUnitTest): self.assertEquals(res.status_int, 403) def test_add_public_image_unauthorized(self): - rules = {"add_image": [], "publicize_image": [["false:false"]]} + rules = {"add_image": '@', "publicize_image": '!'} self.set_policy_rules(rules) fixture_headers = {'x-image-meta-store': 'file', 'x-image-meta-is-public': 'true', @@ -2502,7 +2502,7 @@ class TestGlanceAPI(base.IsolatedUnitTest): def test_publicize_image_unauthorized(self): """Create a non-public image then fail to make public""" - rules = {"add_image": [], "publicize_image": [["false:false"]]} + rules = {"add_image": '@', "publicize_image": '!'} self.set_policy_rules(rules) fixture_headers = {'x-image-meta-store': 'file', @@ -2731,14 +2731,14 @@ class TestGlanceAPI(base.IsolatedUnitTest): self.assertEquals(res.status_int, 400) def test_get_images_detailed_unauthorized(self): - rules = {"get_images": [["false:false"]]} + rules = {"get_images": '!'} self.set_policy_rules(rules) req = webob.Request.blank('/images/detail') res = req.get_response(self.api) self.assertEquals(res.status_int, 403) def test_get_images_unauthorized(self): - rules = {"get_images": [["false:false"]]} + rules = {"get_images": '!'} self.set_policy_rules(rules) req = webob.Request.blank('/images/detail') res = req.get_response(self.api) @@ -2899,7 +2899,7 @@ class TestGlanceAPI(base.IsolatedUnitTest): self.assertEquals(value, res.headers[key]) def test_image_meta_unauthorized(self): - rules = {"get_image": [["false:false"]]} + rules = {"get_image": '!'} self.set_policy_rules(rules) req = webob.Request.blank("/images/%s" % UUID2) req.method = 'HEAD' @@ -2919,14 +2919,14 @@ class TestGlanceAPI(base.IsolatedUnitTest): self.assertEquals(res.status_int, webob.exc.HTTPNotFound.code) def test_show_image_unauthorized(self): - rules = {"get_image": [["false:false"]]} + rules = {"get_image": '!'} self.set_policy_rules(rules) req = webob.Request.blank("/images/%s" % UUID2) res = req.get_response(self.api) self.assertEqual(res.status_int, 403) def test_show_image_unauthorized_download(self): - rules = {"download_image": [["false:false"]]} + rules = {"download_image": '!'} self.set_policy_rules(rules) req = webob.Request.blank("/images/%s" % UUID2) res = req.get_response(self.api) @@ -3054,7 +3054,7 @@ class TestGlanceAPI(base.IsolatedUnitTest): self.assertEquals(res.status_int, httplib.FORBIDDEN) def test_delete_image_unauthorized(self): - rules = {"delete_image": [["false:false"]]} + rules = {"delete_image": '!'} self.set_policy_rules(rules) req = webob.Request.blank("/images/%s" % UUID2) req.method = 'DELETE'