240 lines
11 KiB
Python
240 lines
11 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.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.pecan_wsgi.hooks import utils
|
|
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(method, neutron_context, controller,
|
|
collection, resource, resource_id,
|
|
parent_id=None):
|
|
field_list = []
|
|
if method == 'PUT':
|
|
attrs = controller.resource_info
|
|
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(collection)
|
|
if plugin:
|
|
if utils.is_member_action(controller):
|
|
getter = controller.parent_controller.plugin_shower
|
|
else:
|
|
getter = controller.plugin_shower
|
|
getter_args = [neutron_context, resource_id]
|
|
if parent_id:
|
|
getter_args.append(parent_id)
|
|
return getter(*getter_args, 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
|
|
controller = utils.get_controller(state)
|
|
if not controller or utils.is_member_action(controller):
|
|
return
|
|
collection = state.request.context.get('collection')
|
|
needs_prefetch = (state.request.method == 'PUT' or
|
|
state.request.method == 'DELETE')
|
|
policy.init()
|
|
|
|
action = controller.plugin_handlers[
|
|
pecan_constants.ACTION_MAP[state.request.method]]
|
|
|
|
# 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')
|
|
parent_id = state.request.context.get('parent_id')
|
|
method = state.request.method
|
|
resource_obj = fetch_resource(method, neutron_context, controller,
|
|
collection, resource, resource_id,
|
|
parent_id=parent_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.
|
|
orig_item_tenant_id = item.get('tenant_id')
|
|
if (needs_prefetch and
|
|
(neutron_context.tenant_id != orig_item_tenant_id or
|
|
orig_item_tenant_id is None)):
|
|
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')
|
|
controller = utils.get_controller(state)
|
|
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
|
|
if state.request.method not in pecan_constants.ACTION_MAP:
|
|
return
|
|
if not data or (resource not in data and collection not in data):
|
|
return
|
|
is_single = resource in data
|
|
action_type = pecan_constants.ACTION_MAP[state.request.method]
|
|
if action_type == 'get' and is_single:
|
|
action = controller.plugin_handlers[controller.SHOW]
|
|
elif action_type == 'get':
|
|
action = controller.plugin_handlers[controller.LIST]
|
|
else:
|
|
action = controller.plugin_handlers[action_type]
|
|
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(collection)
|
|
try:
|
|
resp = [self._get_filtered_item(state.request, controller,
|
|
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(str(e))
|
|
|
|
if is_single:
|
|
resp = resp[0]
|
|
state.response.json = {key: resp}
|
|
|
|
def _get_filtered_item(self, request, controller, resource, collection,
|
|
data):
|
|
neutron_context = request.context.get('neutron_context')
|
|
to_exclude = self._exclude_attributes_by_policy(
|
|
neutron_context, controller, resource, collection, data)
|
|
return self._filter_attributes(request, data, to_exclude)
|
|
|
|
def _filter_attributes(self, request, data, fields_to_strip):
|
|
# This routine will remove the fields that were requested to the
|
|
# plugin for policy evaluation but were not specified in the
|
|
# API request
|
|
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, controller, 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 = controller.resource_info.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
|