From 6acc9f6b1983d427445c4ae3248f945115afe1e0 Mon Sep 17 00:00:00 2001 From: "Kevin L. Mitchell" Date: Fri, 12 Oct 2012 13:41:52 -0500 Subject: [PATCH] Update policies Merge in update openstack-common policy code. Updates Nova-specific policy glue code to eliminate deprecated openstack-common policy interfaces. Also cleans up policy code to allow for returning fine-grained policy values. As a side effect, fixes bug 1039132. Change-Id: I2951a0de3751bd2ec868e7a661070fed624e4af2 --- nova/openstack/common/policy.py | 903 ++++++++++++++++++++++++-------- nova/policy.py | 56 +- nova/tests/policy.json | 328 ++++++------ 3 files changed, 889 insertions(+), 398 deletions(-) diff --git a/nova/openstack/common/policy.py b/nova/openstack/common/policy.py index 4b3b2c856..496ed972d 100644 --- a/nova/openstack/common/policy.py +++ b/nova/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 nova.openstack.common.gettextutils import _ @@ -28,217 +70,650 @@ from nova.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/nova/policy.py b/nova/policy.py index d7545b2ee..c9555fbf7 100644 --- a/nova/policy.py +++ b/nova/policy.py @@ -60,15 +60,15 @@ def init(): if not _POLICY_PATH: raise exception.ConfigNotFound(path=FLAGS.policy_file) utils.read_cached_file(_POLICY_PATH, _POLICY_CACHE, - reload_func=_set_brain) + reload_func=_set_rules) -def _set_brain(data): +def _set_rules(data): default_rule = FLAGS.policy_default_rule - policy.set_brain(policy.Brain.load_json(data, default_rule)) + policy.set_rules(policy.Rules.load_json(data, default_rule)) -def enforce(context, action, target): +def enforce(context, action, target, do_raise=True): """Verifies that the action is valid on the target in this context. :param context: nova context @@ -77,26 +77,29 @@ def enforce(context, action, target): i.e. ``compute:create_instance``, ``compute:attach_volume``, ``volume:attach_volume`` - - :param object: dictionary representing the object of the action + :param target: dictionary representing the object of the action for object creation this should be a dictionary representing the location of the object e.g. ``{'project_id': context.project_id}`` + :param do_raise: if True (the default), raises PolicyNotAuthorized; + if False, returns False - :raises nova.exception.PolicyNotAuthorized: if verification fails. + :raises nova.exception.PolicyNotAuthorized: if verification fails + and do_raise is True. + :return: returns a non-False value (not necessarily "True") if + authorized, and the exact value False if not authorized and + do_raise is False. """ init() - match_list = ('rule:%s' % action,) credentials = context.to_dict() - # NOTE(vish): This is to work around the following launchpad bug: - # https://bugs.launchpad.net/openstack-common/+bug/1039132 - # It can be removed when that bug is fixed. - credentials['is_admin'] = unicode(credentials['is_admin']) + # Add the exception arguments if asked to do a raise + extra = {} + if do_raise: + extra.update(exc=exception.PolicyNotAuthorized, action=action) - policy.enforce(match_list, target, credentials, - exception.PolicyNotAuthorized, action=action) + return policy.check(action, target, credentials, **extra) def check_is_admin(roles): @@ -105,15 +108,24 @@ def check_is_admin(roles): """ init() - action = 'context_is_admin' - match_list = ('rule:%s' % action,) target = {} credentials = {'roles': roles} - try: - policy.enforce(match_list, target, credentials, - exception.PolicyNotAuthorized, action=action) - except exception.PolicyNotAuthorized: - return False + return policy.check('context_is_admin', target, credentials) - return True + +@policy.register('is_admin') +class IsAdminCheck(policy.Check): + """An explicit check for is_admin.""" + + def __init__(self, kind, match): + """Initialize the check.""" + + self.expected = (match.lower() == 'true') + + super(IsAdminCheck, self).__init__(kind, str(self.expected)) + + def __call__(self, target, creds): + """Determine whether is_admin matches the requested value.""" + + return creds['is_admin'] == self.expected diff --git a/nova/tests/policy.json b/nova/tests/policy.json index b856da58a..31b9cefd1 100644 --- a/nova/tests/policy.json +++ b/nova/tests/policy.json @@ -1,198 +1,198 @@ { - "admin_api": [["role:admin"]], + "admin_api": "role:admin", - "context_is_admin": [["role:admin"], ["role:administrator"]], - "compute:create": [], - "compute:create:attach_network": [], - "compute:create:attach_volume": [], + "context_is_admin": "role:admin or role:administrator", + "compute:create": "", + "compute:create:attach_network": "", + "compute:create:attach_volume": "", - "compute:get": [], - "compute:get_all": [], + "compute:get": "", + "compute:get_all": "", - "compute:update": [], + "compute:update": "", - "compute:get_instance_metadata": [], - "compute:update_instance_metadata": [], - "compute:delete_instance_metadata": [], + "compute:get_instance_metadata": "", + "compute:update_instance_metadata": "", + "compute:delete_instance_metadata": "", - "compute:get_instance_faults": [], - "compute:get_diagnostics": [], + "compute:get_instance_faults": "", + "compute:get_diagnostics": "", - "compute:get_lock": [], - "compute:lock": [], - "compute:unlock": [], + "compute:get_lock": "", + "compute:lock": "", + "compute:unlock": "", - "compute:get_vnc_console": [], - "compute:get_console_output": [], + "compute:get_vnc_console": "", + "compute:get_console_output": "", - "compute:associate_floating_ip": [], - "compute:reset_network": [], - "compute:inject_network_info": [], - "compute:add_fixed_ip": [], - "compute:remove_fixed_ip": [], + "compute:associate_floating_ip": "", + "compute:reset_network": "", + "compute:inject_network_info": "", + "compute:add_fixed_ip": "", + "compute:remove_fixed_ip": "", - "compute:attach_volume": [], - "compute:detach_volume": [], + "compute:attach_volume": "", + "compute:detach_volume": "", - "compute:inject_file": [], + "compute:inject_file": "", - "compute:set_admin_password": [], + "compute:set_admin_password": "", - "compute:rescue": [], - "compute:unrescue": [], + "compute:rescue": "", + "compute:unrescue": "", - "compute:suspend": [], - "compute:resume": [], + "compute:suspend": "", + "compute:resume": "", - "compute:pause": [], - "compute:unpause": [], + "compute:pause": "", + "compute:unpause": "", - "compute:start": [], - "compute:stop": [], + "compute:start": "", + "compute:stop": "", - "compute:resize": [], - "compute:confirm_resize": [], - "compute:revert_resize": [], + "compute:resize": "", + "compute:confirm_resize": "", + "compute:revert_resize": "", - "compute:rebuild": [], + "compute:rebuild": "", - "compute:reboot": [], + "compute:reboot": "", - "compute:snapshot": [], - "compute:backup": [], + "compute:snapshot": "", + "compute:backup": "", - "compute:security_groups:add_to_instance": [], - "compute:security_groups:remove_from_instance": [], + "compute:security_groups:add_to_instance": "", + "compute:security_groups:remove_from_instance": "", - "compute:delete": [], - "compute:soft_delete": [], - "compute:force_delete": [], - "compute:restore": [], + "compute:delete": "", + "compute:soft_delete": "", + "compute:force_delete": "", + "compute:restore": "", - "compute_extension:accounts": [], - "compute_extension:admin_actions:pause": [], - "compute_extension:admin_actions:unpause": [], - "compute_extension:admin_actions:suspend": [], - "compute_extension:admin_actions:resume": [], - "compute_extension:admin_actions:lock": [], - "compute_extension:admin_actions:unlock": [], - "compute_extension:admin_actions:resetNetwork": [], - "compute_extension:admin_actions:injectNetworkInfo": [], - "compute_extension:admin_actions:createBackup": [], - "compute_extension:admin_actions:migrateLive": [], - "compute_extension:admin_actions:resetState": [], - "compute_extension:admin_actions:migrate": [], - "compute_extension:aggregates": [], - "compute_extension:certificates": [], - "compute_extension:cloudpipe": [], - "compute_extension:config_drive": [], - "compute_extension:console_output": [], - "compute_extension:consoles": [], - "compute_extension:createserverext": [], - "compute_extension:deferred_delete": [], - "compute_extension:disk_config": [], - "compute_extension:extended_server_attributes": [], - "compute_extension:extended_status": [], - "compute_extension:flavor_access": [], - "compute_extension:flavor_disabled": [], - "compute_extension:flavor_rxtx": [], - "compute_extension:flavor_swap": [], - "compute_extension:flavorextradata": [], - "compute_extension:flavorextraspecs": [], - "compute_extension:flavormanage": [], - "compute_extension:floating_ip_dns": [], - "compute_extension:floating_ip_pools": [], - "compute_extension:floating_ips": [], - "compute_extension:hosts": [], - "compute_extension:hypervisors": [], - "compute_extension:instance_usage_audit_log": [], - "compute_extension:keypairs": [], - "compute_extension:multinic": [], - "compute_extension:networks": [], - "compute_extension:networks:view": [], - "compute_extension:quotas:show": [], - "compute_extension:quotas:update": [], - "compute_extension:quota_classes": [], - "compute_extension:rescue": [], - "compute_extension:security_groups": [], - "compute_extension:server_diagnostics": [], - "compute_extension:simple_tenant_usage:show": [], - "compute_extension:simple_tenant_usage:list": [], - "compute_extension:users": [], - "compute_extension:virtual_interfaces": [], - "compute_extension:virtual_storage_arrays": [], - "compute_extension:volumes": [], - "compute_extension:volumetypes": [], - "compute_extension:zones": [], + "compute_extension:accounts": "", + "compute_extension:admin_actions:pause": "", + "compute_extension:admin_actions:unpause": "", + "compute_extension:admin_actions:suspend": "", + "compute_extension:admin_actions:resume": "", + "compute_extension:admin_actions:lock": "", + "compute_extension:admin_actions:unlock": "", + "compute_extension:admin_actions:resetNetwork": "", + "compute_extension:admin_actions:injectNetworkInfo": "", + "compute_extension:admin_actions:createBackup": "", + "compute_extension:admin_actions:migrateLive": "", + "compute_extension:admin_actions:resetState": "", + "compute_extension:admin_actions:migrate": "", + "compute_extension:aggregates": "", + "compute_extension:certificates": "", + "compute_extension:cloudpipe": "", + "compute_extension:config_drive": "", + "compute_extension:console_output": "", + "compute_extension:consoles": "", + "compute_extension:createserverext": "", + "compute_extension:deferred_delete": "", + "compute_extension:disk_config": "", + "compute_extension:extended_server_attributes": "", + "compute_extension:extended_status": "", + "compute_extension:flavor_access": "", + "compute_extension:flavor_disabled": "", + "compute_extension:flavor_rxtx": "", + "compute_extension:flavor_swap": "", + "compute_extension:flavorextradata": "", + "compute_extension:flavorextraspecs": "", + "compute_extension:flavormanage": "", + "compute_extension:floating_ip_dns": "", + "compute_extension:floating_ip_pools": "", + "compute_extension:floating_ips": "", + "compute_extension:hosts": "", + "compute_extension:hypervisors": "", + "compute_extension:instance_usage_audit_log": "", + "compute_extension:keypairs": "", + "compute_extension:multinic": "", + "compute_extension:networks": "", + "compute_extension:networks:view": "", + "compute_extension:quotas:show": "", + "compute_extension:quotas:update": "", + "compute_extension:quota_classes": "", + "compute_extension:rescue": "", + "compute_extension:security_groups": "", + "compute_extension:server_diagnostics": "", + "compute_extension:simple_tenant_usage:show": "", + "compute_extension:simple_tenant_usage:list": "", + "compute_extension:users": "", + "compute_extension:virtual_interfaces": "", + "compute_extension:virtual_storage_arrays": "", + "compute_extension:volumes": "", + "compute_extension:volumetypes": "", + "compute_extension:zones": "", - "volume:create": [], - "volume:get": [], - "volume:get_all": [], - "volume:get_volume_metadata": [], - "volume:delete": [], - "volume:update": [], - "volume:delete_volume_metadata": [], - "volume:update_volume_metadata": [], - "volume:attach": [], - "volume:detach": [], - "volume:reserve_volume": [], - "volume:unreserve_volume": [], - "volume:begin_detaching": [], - "volume:roll_detaching": [], - "volume:check_attach": [], - "volume:check_detach": [], - "volume:initialize_connection": [], - "volume:terminate_connection": [], - "volume:create_snapshot": [], - "volume:delete_snapshot": [], - "volume:get_snapshot": [], - "volume:get_all_snapshots": [], + "volume:create": "", + "volume:get": "", + "volume:get_all": "", + "volume:get_volume_metadata": "", + "volume:delete": "", + "volume:update": "", + "volume:delete_volume_metadata": "", + "volume:update_volume_metadata": "", + "volume:attach": "", + "volume:detach": "", + "volume:reserve_volume": "", + "volume:unreserve_volume": "", + "volume:begin_detaching": "", + "volume:roll_detaching": "", + "volume:check_attach": "", + "volume:check_detach": "", + "volume:initialize_connection": "", + "volume:terminate_connection": "", + "volume:create_snapshot": "", + "volume:delete_snapshot": "", + "volume:get_snapshot": "", + "volume:get_all_snapshots": "", - "volume_extension:volume_admin_actions:reset_status": [["rule:admin_api"]], - "volume_extension:snapshot_admin_actions:reset_status": [["rule:admin_api"]], - "volume_extension:volume_admin_actions:force_delete": [["rule:admin_api"]], - "volume_extension:volume_actions:upload_image": [], - "volume_extension:types_manage": [], - "volume_extension:types_extra_specs": [], + "volume_extension:volume_admin_actions:reset_status": "rule:admin_api", + "volume_extension:snapshot_admin_actions:reset_status": "rule:admin_api", + "volume_extension:volume_admin_actions:force_delete": "rule:admin_api", + "volume_extension:volume_actions:upload_image": "", + "volume_extension:types_manage": "", + "volume_extension:types_extra_specs": "", - "network:get_all_networks": [], - "network:get_network": [], - "network:delete_network": [], - "network:disassociate_network": [], - "network:get_vifs_by_instance": [], - "network:allocate_for_instance": [], - "network:deallocate_for_instance": [], - "network:validate_networks": [], - "network:get_instance_uuids_by_ip_filter": [], + "network:get_all_networks": "", + "network:get_network": "", + "network:delete_network": "", + "network:disassociate_network": "", + "network:get_vifs_by_instance": "", + "network:allocate_for_instance": "", + "network:deallocate_for_instance": "", + "network:validate_networks": "", + "network:get_instance_uuids_by_ip_filter": "", - "network:get_floating_ip": [], - "network:get_floating_ip_pools": [], - "network:get_floating_ip_by_address": [], - "network:get_floating_ips_by_project": [], - "network:get_floating_ips_by_fixed_address": [], - "network:allocate_floating_ip": [], - "network:deallocate_floating_ip": [], - "network:associate_floating_ip": [], - "network:disassociate_floating_ip": [], + "network:get_floating_ip": "", + "network:get_floating_ip_pools": "", + "network:get_floating_ip_by_address": "", + "network:get_floating_ips_by_project": "", + "network:get_floating_ips_by_fixed_address": "", + "network:allocate_floating_ip": "", + "network:deallocate_floating_ip": "", + "network:associate_floating_ip": "", + "network:disassociate_floating_ip": "", - "network:get_fixed_ip": [], - "network:get_fixed_ip_by_address": [], - "network:add_fixed_ip_to_instance": [], - "network:remove_fixed_ip_from_instance": [], - "network:add_network_to_project": [], - "network:get_instance_nw_info": [], + "network:get_fixed_ip": "", + "network:get_fixed_ip_by_address": "", + "network:add_fixed_ip_to_instance": "", + "network:remove_fixed_ip_from_instance": "", + "network:add_network_to_project": "", + "network:get_instance_nw_info": "", - "network:get_dns_domains": [], - "network:add_dns_entry": [], - "network:modify_dns_entry": [], - "network:delete_dns_entry": [], - "network:get_dns_entries_by_address": [], - "network:get_dns_entries_by_name": [], - "network:create_private_dns_domain": [], - "network:create_public_dns_domain": [], - "network:delete_dns_domain": [] + "network:get_dns_domains": "", + "network:add_dns_entry": "", + "network:modify_dns_entry": "", + "network:delete_dns_entry": "", + "network:get_dns_entries_by_address": "", + "network:get_dns_entries_by_name": "", + "network:create_private_dns_domain": "", + "network:create_public_dns_domain": "", + "network:delete_dns_domain": "" }