From 1736361860933b0e05443e6a9efce8d9275e5c30 Mon Sep 17 00:00:00 2001 From: Kevin Benton Date: Thu, 11 Jun 2015 07:13:12 -0700 Subject: [PATCH] Add hook for policy enforcement Add a hook to enforce policy.json. Partially-Implements: blueprint wsgi-pecan-switch Change-Id: I49096f2e6ea70477dbbd399fd81f33bd19dbc453 --- neutron/newapi/app.py | 1 + neutron/newapi/controllers/root.py | 2 - neutron/newapi/hooks/__init__.py | 2 + neutron/newapi/hooks/policy_enforcement.py | 126 ++++++++++++++++++ .../functional/newapi/test_functional.py | 4 + 5 files changed, 133 insertions(+), 2 deletions(-) create mode 100644 neutron/newapi/hooks/policy_enforcement.py diff --git a/neutron/newapi/app.py b/neutron/newapi/app.py index b41439b48..23fa57756 100644 --- a/neutron/newapi/app.py +++ b/neutron/newapi/app.py @@ -48,6 +48,7 @@ def setup_app(*args, **kwargs): hooks.AttributePopulationHook(), # priority 120 hooks.OwnershipValidationHook(), # priority 125 hooks.QuotaEnforcementHook(), # priority 130 + hooks.PolicyHook(), # priority 135 ] app = pecan.make_app( diff --git a/neutron/newapi/controllers/root.py b/neutron/newapi/controllers/root.py index a9ba2c62a..221755f94 100644 --- a/neutron/newapi/controllers/root.py +++ b/neutron/newapi/controllers/root.py @@ -62,8 +62,6 @@ class GeneralController(object): @expose(generic=True) def index(self): - if pecan.request.method != 'GET': - pecan.abort(405) return self.get() def get(self): diff --git a/neutron/newapi/hooks/__init__.py b/neutron/newapi/hooks/__init__.py index 0b06df40c..975c98315 100644 --- a/neutron/newapi/hooks/__init__.py +++ b/neutron/newapi/hooks/__init__.py @@ -16,6 +16,7 @@ from neutron.newapi.hooks import attribute_population from neutron.newapi.hooks import context from neutron.newapi.hooks import ownership_validation +from neutron.newapi.hooks import policy_enforcement from neutron.newapi.hooks import quota_enforcement from neutron.newapi.hooks import resource_identifier from neutron.newapi.hooks import translation @@ -26,4 +27,5 @@ ContextHook = context.ContextHook ResourceIdentifierHook = resource_identifier.ResourceIdentifierHook AttributePopulationHook = attribute_population.AttributePopulationHook OwnershipValidationHook = ownership_validation.OwnershipValidationHook +PolicyHook = policy_enforcement.PolicyHook QuotaEnforcementHook = quota_enforcement.QuotaEnforcementHook diff --git a/neutron/newapi/hooks/policy_enforcement.py b/neutron/newapi/hooks/policy_enforcement.py new file mode 100644 index 000000000..4b0696c33 --- /dev/null +++ b/neutron/newapi/hooks/policy_enforcement.py @@ -0,0 +1,126 @@ +# Copyright (c) 2015 Mirantis, Inc. +# 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. + +import copy +import simplejson + +from oslo_policy import policy as oslo_policy +from oslo_utils import excutils +import pecan +from pecan import hooks +import webob + +from neutron.common import constants as const +from neutron.newapi.hooks import attribute_population +from neutron import policy + + +class PolicyHook(hooks.PecanHook): + priority = 135 + ACTION_MAP = {'POST': 'create', 'PUT': 'update', 'GET': 'get', + 'DELETE': 'delete'} + + def before(self, state): + if state.request.method not in self.ACTION_MAP: + pecan.abort(405) + rtype = state.request.resource_type + if not rtype: + return + is_update = (state.request.method == 'PUT') + items = state.request.resources + policy.init() + action = '%s_%s' % (self.ACTION_MAP[state.request.method], rtype) + for item in items: + if is_update: + obj = copy.copy(state.request.original_object) + obj.update(item) + obj[const.ATTRIBUTES_TO_UPDATE] = item.keys() + item = obj + try: + policy.enforce(state.request.context, action, item, + pluralized=attribute_population._plural(rtype)) + except oslo_policy.PolicyNotAuthorized: + with excutils.save_and_reraise_exception() as ctxt: + # If a tenant is modifying it's own object, it's safe to + # return a 403. Otherwise, pretend that it doesn't exist + # to avoid giving away information. + context = state.request.context + if (is_update and + context.tenant_id != obj['tenant_id']): + ctxt.reraise = False + msg = _('The resource could not be found.') + raise webob.exc.HTTPNotFound(msg) + + def after(self, state): + resource_type = getattr(state.request, 'resource_type', None) + if not resource_type: + # can't filter a resource we don't recognize + return + # NOTE(kevinbenton): extension listing isn't controlled by policy + if resource_type == 'extension': + return + try: + data = state.response.json + except simplejson.JSONDecodeError: + return + if not data: + return + if resource_type in data: + # singular response + data[resource_type] = self._get_filtered_item( + state.request.context, resource_type, data[resource_type]) + elif attribute_population._plural(resource_type) in data: + # plural response + plural = attribute_population._plural(resource_type) + data[plural] = [self._get_filtered_item(state.request.context, + resource_type, item) + for item in data[plural]] + state.response.json = data + + def _get_filtered_item(self, context, resource_type, data): + to_exclude = self._exclude_attributes_by_policy(context, + resource_type, data) + return self._filter_attributes(context, data, to_exclude) + + def _filter_attributes(self, context, data, fields_to_strip): + return dict(item for item in data.items() + if (item[0] not in fields_to_strip)) + + def _exclude_attributes_by_policy(self, context, resource_type, data): + """Identifies attributes to exclude according to authZ policies. + + Return a list of attribute names which should be stripped from the + response returned to the user because the user is not authorized + to see them. + """ + attributes_to_exclude = [] + for attr_name in data.keys(): + attr_data = attribute_population._attributes_for_resource( + resource_type).get(attr_name) + if attr_data and attr_data['is_visible']: + if policy.check( + context, + # NOTE(kevinbenton): this used to reference a + # _plugin_handlers dict, why? + 'get_%s:%s' % (resource_type, attr_name), + data, + might_not_exist=True, + pluralized=attribute_population._plural(resource_type)): + # this attribute is visible, check next one + continue + # if the code reaches this point then either the policy check + # failed or the attribute was not visible in the first place + attributes_to_exclude.append(attr_name) + return attributes_to_exclude diff --git a/neutron/tests/functional/newapi/test_functional.py b/neutron/tests/functional/newapi/test_functional.py index 617e52c9e..bc7089c8d 100644 --- a/neutron/tests/functional/newapi/test_functional.py +++ b/neutron/tests/functional/newapi/test_functional.py @@ -217,3 +217,7 @@ class TestEnforcementHooks(PecanFunctionalTest): def test_quota_enforcement(self): # TODO(kevinbenton): this test should do something pass + + def test_policy_enforcement(self): + # TODO(kevinbenton): this test should do something + pass -- 2.45.2