--- /dev/null
+{
+ "admin_or_owner": [["role:admin"], ["tenant_id:%(tenant_id)s"]],
+ "default": [["rule:admin_or_owner"]],
+
+ "create_subnet": [],
+ "get_subnet": [["rule:admin_or_owner"]],
+ "update_subnet": [["rule:admin_or_owner"]],
+ "delete_subnet": [["rule:admin_or_owner"]],
+
+ "create_network": [],
+ "get_network": [["rule:admin_or_owner"]],
+ "update_network": [["rule:admin_or_owner"]],
+ "delete_network": [["rule:admin_or_owner"]],
+
+ "create_port": [],
+ "get_port": [["rule:admin_or_owner"]],
+ "update_port": [["rule:admin_or_owner"]],
+ "delete_port": [["rule:admin_or_owner"]]
+}
[DEFAULT]
# The list of modules to copy from openstack-common
-modules=cfg,exception,importutils,iniparser,jsonutils,setup
+modules=cfg,exception,importutils,iniparser,jsonutils,policy,setup
# The base module to hold the copy of openstack.common
base=quantum
import webob.exc
-from quantum.common import exceptions
from quantum.api.v2 import resource as wsgi_resource
-from quantum.common import utils
from quantum.api.v2 import views
+from quantum.common import exceptions
+from quantum.common import utils
+from quantum import policy
LOG = logging.getLogger(__name__)
XML_NS_V20 = 'http://openstack.org/quantum/api/v2.0'
self._attr_info = attr_info
self._view = getattr(views, self._resource)
- def _items(self, request):
+ def _items(self, request, do_authz=False):
"""Retrieves and formats a list of elements of the requested entity"""
kwargs = {'filters': filters(request),
'verbose': verbose(request),
obj_getter = getattr(self._plugin, "get_%s" % self._collection)
obj_list = obj_getter(request.context, **kwargs)
+
+ # Check authz
+ if do_authz:
+ # Omit items from list that should not be visible
+ obj_list = [obj for obj in obj_list
+ if policy.check(request.context,
+ "get_%s" % self._resource,
+ obj)]
+
return {self._collection: [self._view(obj) for obj in obj_list]}
- def _item(self, request, id):
+ def _item(self, request, id, do_authz=False):
"""Retrieves and formats a single element of the requested entity"""
kwargs = {'verbose': verbose(request),
'fields': fields(request)}
- obj_getter = getattr(self._plugin,
- "get_%s" % self._resource)
+ action = "get_%s" % self._resource
+ obj_getter = getattr(self._plugin, action)
obj = obj_getter(request.context, id, **kwargs)
+
+ # Check authz
+ if do_authz:
+ policy.enforce(request.context, action, obj)
+
return {self._resource: self._view(obj)}
def index(self, request):
"""Returns a list of the requested entity"""
- return self._items(request)
+ return self._items(request, True)
def show(self, request, id):
"""Returns detailed information about the requested entity"""
- return self._item(request, id)
+ try:
+ return self._item(request, id, True)
+ except exceptions.PolicyNotAuthorized:
+ # To avoid giving away information, pretend that it
+ # doesn't exist
+ raise webob.exc.HTTPNotFound()
def create(self, request, body=None):
"""Creates a new instance of the requested entity"""
body = self._prepare_request_body(request.context, body, True,
allow_bulk=True)
- obj_creator = getattr(self._plugin,
- "create_%s" % self._resource)
+
+ action = "create_%s" % self._resource
+
+ # Check authz
+ try:
+ if self._collection in body:
+ # Have to account for bulk create
+ for item in body[self._collection]:
+ policy.enforce(request.context, action,
+ item[self._resource])
+ else:
+ policy.enforce(request.context, action, body[self._resource])
+ except exceptions.PolicyNotAuthorized:
+ raise webob.exc.HTTPForbidden()
+
+ obj_creator = getattr(self._plugin, action)
kwargs = {self._resource: body}
obj = obj_creator(request.context, **kwargs)
return {self._resource: self._view(obj)}
def delete(self, request, id):
"""Deletes the specified entity"""
- obj_deleter = getattr(self._plugin,
- "delete_%s" % self._resource)
+ action = "delete_%s" % self._resource
+
+ # Check authz
+ obj = self._item(request, id)
+ try:
+ policy.enforce(request.context, action, obj)
+ except exceptions.PolicyNotAuthorized:
+ # To avoid giving away information, pretend that it
+ # doesn't exist
+ raise webob.exc.HTTPNotFound()
+
+ obj_deleter = getattr(self._plugin, action)
obj_deleter(request.context, id)
def update(self, request, id, body=None):
"""Updates the specified entity's attributes"""
body = self._prepare_request_body(request.context, body, False)
- obj_updater = getattr(self._plugin,
- "update_%s" % self._resource)
+ action = "update_%s" % self._resource
+
+ # Check authz
+ orig_obj = self._item(request, id)
+ try:
+ policy.enforce(request.context, action, orig_obj)
+ except exceptions.PolicyNotAuthorized:
+ # To avoid giving away information, pretend that it
+ # doesn't exist
+ raise webob.exc.HTTPNotFound()
+
+ obj_updater = getattr(self._plugin, action)
kwargs = {self._resource: body}
obj = obj_updater(request.context, id, **kwargs)
return {self._resource: self._view(obj)}
message = _("User does not have admin privileges: %(reason)s")
+class PolicyNotAuthorized(NotAuthorized):
+ message = _("Policy doesn't allow %(action)s to be performed.")
+
+
class ClassNotFound(NotFound):
message = _("Class %(class_name)s could not be found")
"on network %(net_id)s")
+class PolicyNotFound(NotFound):
+ message = _("Policy configuration policy.json could not be found")
+
+
class StateInvalid(QuantumException):
message = _("Unsupported port state: %(port_state)s")
--- /dev/null
+# 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 logging
+import urllib
+import urllib2
+
+
+LOG = logging.getLogger(__name__)
+
+
+_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, exc=None,
+ *args, **kwargs):
+ """Enforces authorization of some rules against credentials.
+
+ :param match_list: nested tuples of data to match against
+
+ The basic brain supports three types of match lists:
+
+ 1) rules
+
+ looks like: ``('rule:compute:get_instance',)``
+
+ Retrieves the named rule from the rules dict and recursively
+ checks against the contents of the rule.
+
+ 2) roles
+
+ looks like: ``('role:compute:admin',)``
+
+ Matches if the specified role is in credentials_dict['roles'].
+
+ 3) generic
+
+ looks like: ``('tenant_id:%(tenant_id)s',)``
+
+ Substitutes values from the target dict into the match using
+ the % operator and matches them against the creds dict.
+
+ Combining rules:
+
+ The brain returns True if any of the outer tuple of rules
+ match and also True if all of the inner tuples match. You
+ can use this to perform simple boolean logic. For
+ example, the following rule would return True if the creds
+ contain the role 'admin' OR the if the tenant_id matches
+ the target dict AND the the creds contains the role
+ 'compute_sysadmin':
+
+ ::
+
+ {
+ "rule:combined": (
+ 'role:admin',
+ ('tenant_id:%(tenant_id)s', 'role:compute_sysadmin')
+ )
+ }
+
+ Note that rule and role are reserved words in the credentials match, so
+ you can't match against properties with those names. Custom brains may
+ also add new reserved words. For example, the HttpBrain adds http as a
+ reserved word.
+
+ :param target_dict: dict of object properties
+
+ Target dicts contain as much information as we can about the object being
+ operated on.
+
+ :param credentials_dict: dict of actor properties
+
+ Credentials dicts contain as much information as we can about the user
+ performing the action.
+
+ :param exc: exception to raise
+
+ Class of the exception to raise if the check fails. Any remaining
+ arguments passed to enforce() (both positional and keyword arguments)
+ will be passed to the exception class. If exc is not provided, returns
+ False.
+
+ :return: True if the policy allows the action
+ :return: False if the policy does not allow the action and exc is not set
+ """
+ global _BRAIN
+ if not _BRAIN:
+ _BRAIN = Brain()
+ if not _BRAIN.check(match_list, target_dict, credentials_dict):
+ if exc:
+ raise exc(*args, **kwargs)
+ return False
+ return True
+
+
+class Brain(object):
+ """Implements policy checking."""
+ @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):
+ 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
+ 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"
--- /dev/null
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright (c) 2012 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.
+
+"""
+Policy engine for quantum. Largely copied from nova.
+"""
+
+import os.path
+
+from quantum.common import config
+from quantum.common import exceptions
+from quantum.openstack.common import policy
+
+
+_POLICY_PATH = None
+
+
+def reset():
+ global _POLICY_PATH
+ _POLICY_PATH = None
+ policy.reset()
+
+
+def init():
+ global _POLICY_PATH
+ if not _POLICY_PATH:
+ _POLICY_PATH = config.find_config_file({}, [], 'policy.json')
+ if not _POLICY_PATH:
+ raise exceptions.PolicyNotFound(path=FLAGS.policy_file)
+ with open(_POLICY_PATH) as f:
+ _set_brain(f.read())
+
+
+def _set_brain(data):
+ default_rule = 'default'
+ policy.set_brain(policy.HttpBrain.load_json(data, default_rule))
+
+
+def check(context, action, target):
+ """Verifies that the action is valid on the target in this context.
+
+ :param context: quantum context
+ :param action: string representing the action to be checked
+ this should be colon separated for clarity.
+ :param object: 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}``
+
+ :return: Returns True if access is permitted else False.
+ """
+
+ init()
+
+ match_list = ('rule:%s' % action,)
+ credentials = context.to_dict()
+
+ return policy.enforce(match_list, target, credentials)
+
+
+def enforce(context, action, target):
+ """Verifies that the action is valid on the target in this context.
+
+ :param context: quantum context
+ :param action: string representing the action to be checked
+ this should be colon separated for clarity.
+ :param object: 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}``
+
+ :raises quantum.exceptions.PolicyNotAllowed: if verification fails.
+ """
+
+ init()
+
+ match_list = ('rule:%s' % action,)
+ credentials = context.to_dict()
+
+ policy.enforce(match_list, target, credentials,
+ exceptions.PolicyNotAuthorized, action=action)