Files
neutron/neutron/pecan_wsgi/hooks/policy_enforcement.py
Kevin Benton fc954c9426 Add Nova notifier hook calls to pecan
This patch adds a the Nova notifier calls to the pecan
notification hook and adds some simple functional tests
for them as well.

This patch also alters the operations of the policy enforcement
hook, which now stores in the request context the original
value of the object rather than the value of the object as it
would have appeared to the plugin - that is to say a merge
between the original objects and the parameters passed on to
the request body. Such 'frankenobject' is indeed of no use for
the notifiers.

Partial-Bug: #1552979
Change-Id: I4a37197eb38afe15d2b368f4e355226824478792
2016-03-08 04:21:31 -08:00

214 lines
9.5 KiB
Python

# 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
from oslo_policy import policy as oslo_policy
from oslo_utils import excutils
from pecan import hooks
import webob
from neutron._i18n import _
from neutron.api.v2 import attributes as v2_attributes
from neutron.common import constants as const
from neutron.extensions import quotasv2
from neutron import manager
from neutron.pecan_wsgi import constants as pecan_constants
from neutron.pecan_wsgi.controllers import quota
from neutron import policy
def _custom_getter(resource, resource_id):
"""Helper function to retrieve resources not served by any plugin."""
if resource == quotasv2.RESOURCE_NAME:
return quota.get_tenant_quotas(resource_id)[quotasv2.RESOURCE_NAME]
def fetch_resource(neutron_context, resource, resource_id):
attrs = v2_attributes.get_resource_info(resource)
if not attrs:
# this isn't a request for a normal resource. it could be
# an action like removing a network from a dhcp agent.
# return None and assume the custom controller for this will
# handle the necessary logic.
return
field_list = [name for (name, value) in attrs.items()
if (value.get('required_by_policy') or
value.get('primary_key') or 'default' not in value)]
plugin = manager.NeutronManager.get_plugin_for_resource(resource)
if plugin:
getter = getattr(plugin, 'get_%s' % resource)
# TODO(kevinbenton): the parent_id logic currently in base.py
return getter(neutron_context, resource_id, fields=field_list)
else:
# Some legit resources, like quota, do not have a plugin yet.
# Retrieving the original object is nevertheless important
# for policy checks.
return _custom_getter(resource, resource_id)
class PolicyHook(hooks.PecanHook):
priority = 140
def before(self, state):
# This hook should be run only for PUT,POST and DELETE methods and for
# requests targeting a neutron resource
resources = state.request.context.get('resources', [])
if state.request.method not in ('POST', 'PUT', 'DELETE'):
return
# As this routine will likely alter the resources, do a shallow copy
resources_copy = resources[:]
neutron_context = state.request.context.get('neutron_context')
resource = state.request.context.get('resource')
# If there is no resource for this request, don't bother running authZ
# policies
if not resource:
return
collection = state.request.context.get('collection')
needs_prefetch = (state.request.method == 'PUT' or
state.request.method == 'DELETE')
policy.init()
action = '%s_%s' % (pecan_constants.ACTION_MAP[state.request.method],
resource)
# NOTE(salv-orlando): As bulk updates are not supported, in case of PUT
# requests there will be only a single item to process, and its
# identifier would have been already retrieved by the lookup process;
# in the case of DELETE requests there won't be any item to process in
# the request body
original_resources = []
if needs_prefetch:
try:
item = resources_copy.pop()
except IndexError:
# Ops... this was a delete after all!
item = {}
resource_id = state.request.context.get('resource_id')
resource_obj = fetch_resource(neutron_context,
resource, resource_id)
if resource_obj:
original_resources.append(resource_obj)
obj = copy.copy(resource_obj)
obj.update(item)
obj[const.ATTRIBUTES_TO_UPDATE] = item.keys()
# Put back the item in the list so that policies could be
# enforced
resources_copy.append(obj)
# TODO(salv-orlando): as other hooks might need to prefetch resources,
# store them in the request context. However, this should be done in a
# separate hook which is conventietly called before all other hooks
state.request.context['original_resources'] = original_resources
for item in resources_copy:
try:
policy.enforce(
neutron_context, action, item,
pluralized=collection)
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.
if (needs_prefetch and
neutron_context.tenant_id != item['tenant_id']):
ctxt.reraise = False
msg = _('The resource could not be found.')
raise webob.exc.HTTPNotFound(msg)
def after(self, state):
neutron_context = state.request.context.get('neutron_context')
resource = state.request.context.get('resource')
collection = state.request.context.get('collection')
if not resource:
# can't filter a resource we don't recognize
return
# NOTE(kevinbenton): extension listing isn't controlled by policy
if resource == 'extension':
return
try:
data = state.response.json
except ValueError:
return
action = '%s_%s' % (pecan_constants.ACTION_MAP[state.request.method],
resource)
if not data or (resource not in data and collection not in data):
return
is_single = resource in data
key = resource if is_single else collection
to_process = [data[resource]] if is_single else data[collection]
# in the single case, we enforce which raises on violation
# in the plural case, we just check so violating items are hidden
policy_method = policy.enforce if is_single else policy.check
plugin = manager.NeutronManager.get_plugin_for_resource(resource)
try:
resp = [self._get_filtered_item(state.request, resource,
collection, item)
for item in to_process
if (state.request.method != 'GET' or
policy_method(neutron_context, action, item,
plugin=plugin,
pluralized=collection))]
except oslo_policy.PolicyNotAuthorized as e:
# This exception must be explicitly caught as the exception
# translation hook won't be called if an error occurs in the
# 'after' handler.
raise webob.exc.HTTPForbidden(e.message)
if is_single:
resp = resp[0]
state.response.json = {key: resp}
def _get_filtered_item(self, request, resource, collection, data):
neutron_context = request.context.get('neutron_context')
to_exclude = self._exclude_attributes_by_policy(
neutron_context, resource, collection, data)
return self._filter_attributes(request, data, to_exclude)
def _filter_attributes(self, request, data, fields_to_strip):
# TODO(kevinbenton): this works but we didn't allow the plugin to
# only fetch the fields we are interested in. consider moving this
# to the call
user_fields = request.params.getall('fields')
return dict(item for item in data.items()
if (item[0] not in fields_to_strip and
(not user_fields or item[0] in user_fields)))
def _exclude_attributes_by_policy(self, context, resource,
collection, 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 = v2_attributes.get_resource_info(
resource).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, attr_name),
data,
might_not_exist=True,
pluralized=collection):
# 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