diff options
| author | termie <github@anarkystic.com> | 2012-02-28 16:50:48 -0800 |
|---|---|---|
| committer | termie <github@anarkystic.com> | 2012-03-08 14:06:32 -0800 |
| commit | a2f2274c69df2ca5b040a69173f3eb7eb030c561 (patch) | |
| tree | c57294737b6fe1bda4706d95e08863d4f81c958e /keystone/common | |
| parent | e5254d48b133f3ec9798cc8eb48a03cb69ff2d97 (diff) | |
port common policy code to keystone
keystone.common.policy is copied from nova
leave simple backend in as a shim until devstack stops referencing it
Change-Id: Ibd579cfeb99465706d525b6565818a2d8f5f3b7c
Diffstat (limited to 'keystone/common')
| -rw-r--r-- | keystone/common/policy.py | 207 | ||||
| -rw-r--r-- | keystone/common/utils.py | 43 | ||||
| -rw-r--r-- | keystone/common/wsgi.py | 7 |
3 files changed, 254 insertions, 3 deletions
diff --git a/keystone/common/policy.py b/keystone/common/policy.py new file mode 100644 index 00000000..34492f73 --- /dev/null +++ b/keystone/common/policy.py @@ -0,0 +1,207 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright (c) 2011 OpenStack, LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""Common Policy Engine Implementation""" + +import json +import urllib +import urllib2 + + +class NotAuthorized(Exception): + pass + + +_BRAIN = None + + +def set_brain(brain): + """Set the brain used by enforce(). + + Defaults use Brain() if not set. + + """ + global _BRAIN + _BRAIN = brain + + +def reset(): + """Clear the brain used by enforce().""" + global _BRAIN + _BRAIN = None + + +def enforce(match_list, target_dict, credentials_dict): + """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 + ('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. + + :raises NotAuthorized if the check fails + + """ + global _BRAIN + if not _BRAIN: + _BRAIN = Brain() + if not _BRAIN.check(match_list, target_dict, credentials_dict): + raise NotAuthorized() + + +class Brain(object): + """Implements policy checking.""" + @classmethod + def load_json(cls, data, default_rule=None): + """Init a brain using json instead of a rules dictionary.""" + rules_dict = json.loads(data) + return cls(rules=rules_dict, default_rule=default_rule) + + def __init__(self, rules=None, default_rule=None): + self.rules = rules or {} + self.default_rule = default_rule + + def add_rule(self, key, match): + self.rules[key] = match + + def _check(self, match, target_dict, cred_dict): + match_kind, match_value = match.split(':', 1) + try: + f = getattr(self, '_check_%s' % match_kind) + except AttributeError: + if not self._check_generic(match, target_dict, cred_dict): + return False + else: + if not f(match_value, target_dict, cred_dict): + return False + return True + + def check(self, match_list, target_dict, cred_dict): + """Checks authorization of some rules against credentials. + + 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 + + """ + 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 + return False + + def _check_rule(self, match, target_dict, cred_dict): + """Recursively checks credentials based on the brains rules.""" + try: + new_match_list = self.rules[match] + except KeyError: + if self.default_rule and match != self.default_rule: + new_match_list = ('rule:%s' % self.default_rule,) + else: + return False + + return self.check(new_match_list, target_dict, cred_dict) + + def _check_role(self, 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']] + + def _check_generic(self, match, target_dict, cred_dict): + """Check an individual match. + + Matches look like: + + tenant:%(tenant_id)s + role:compute:admin + + """ + + # TODO(termie): do dict inspection via dot syntax + match = match % target_dict + key, value = match.split(':', 1) + if key in cred_dict: + return value == cred_dict[key] + return False + + +class HttpBrain(Brain): + """A brain that can check external urls for policy. + + Posts json blobs for target and credentials. + + """ + + def _check_http(self, match, target_dict, cred_dict): + """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. + + """ + url = match % target_dict + data = {'target': json.dumps(target_dict), + 'credentials': json.dumps(cred_dict)} + post_data = urllib.urlencode(data) + f = urllib2.urlopen(url, post_data) + return f.read() == "True" diff --git a/keystone/common/utils.py b/keystone/common/utils.py index b7d4b9c0..74b490ee 100644 --- a/keystone/common/utils.py +++ b/keystone/common/utils.py @@ -22,6 +22,7 @@ import base64 import hashlib import hmac import json +import os import subprocess import sys import time @@ -61,6 +62,48 @@ def import_object(import_str, *args, **kw): return cls(*args, **kw) +def find_config(config_path): + """Find a configuration file using the given hint. + + :param config_path: Full or relative path to the config. + :returns: Full path of the config, if it exists. + + """ + possible_locations = [ + config_path, + os.path.join('etc', 'keystone', config_path), + os.path.join('etc', config_path), + os.path.join(config_path), + '/etc/keystone/%s' % config_path, + ] + + for path in possible_locations: + if os.path.exists(path): + return os.path.abspath(path) + + raise Exception('Config not found: %s', os.path.abspath(config_path)) + + +def read_cached_file(filename, cache_info, reload_func=None): + """Read from a file if it has been modified. + + :param cache_info: dictionary to hold opaque cache. + :param reload_func: optional function to be called with data when + file is reloaded due to a modification. + + :returns: data from file + + """ + mtime = os.path.getmtime(filename) + if not cache_info or mtime != cache_info.get('mtime'): + with open(filename) as fap: + cache_info['data'] = fap.read() + cache_info['mtime'] = mtime + if reload_func: + reload_func(cache_info['data']) + return cache_info['data'] + + class SmarterEncoder(json.JSONEncoder): """Help for JSON encoding dict-like objects.""" def default(self, obj): diff --git a/keystone/common/wsgi.py b/keystone/common/wsgi.py index f8a7e2f1..73cc3ad9 100644 --- a/keystone/common/wsgi.py +++ b/keystone/common/wsgi.py @@ -214,9 +214,10 @@ class Application(BaseApplication): creds['roles'] = [self.identity_api.get_role(context, role)['name'] for role in creds.get('roles', [])] # Accept either is_admin or the admin role - assert self.policy_api.can_haz(context, - ('is_admin:1', 'roles:admin'), - creds) + self.policy_api.enforce(context, + creds, + 'admin_required', + {}) class Middleware(Application): |
