# 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. import netaddr import webob.exc from quantum.api.v2 import attributes from quantum.api.v2 import resource as wsgi_resource from quantum.common import exceptions from quantum.openstack.common import log as logging from quantum.openstack.common.notifier import api as notifier_api from quantum import policy from quantum import quota LOG = logging.getLogger(__name__) FAULT_MAP = {exceptions.NotFound: webob.exc.HTTPNotFound, exceptions.Conflict: webob.exc.HTTPConflict, exceptions.InUse: webob.exc.HTTPConflict, exceptions.BadRequest: webob.exc.HTTPBadRequest, exceptions.ServiceUnavailable: webob.exc.HTTPServiceUnavailable, exceptions.NotAuthorized: webob.exc.HTTPForbidden, netaddr.AddrFormatError: webob.exc.HTTPBadRequest, AttributeError: webob.exc.HTTPBadRequest, ValueError: webob.exc.HTTPBadRequest, } def _fields(request): """ Extracts the list of fields to return """ return [v for v in request.GET.getall('fields') if v] def _filters(request, attr_info): """ Extracts the filters from the request string Returns a dict of lists for the filters: check=a&check=b&name=Bob& becomes {'check': [u'a', u'b'], 'name': [u'Bob']} """ res = {} for key, values in request.GET.dict_of_lists().iteritems(): if key == 'fields': continue values = [v for v in values if v] key_attr_info = attr_info.get(key, {}) if 'convert_list_to' in key_attr_info: values = key_attr_info['convert_list_to'](values) elif 'convert_to' in key_attr_info: convert_to = key_attr_info['convert_to'] values = [convert_to(v) for v in values] if values: res[key] = values return res class Controller(object): LIST = 'list' SHOW = 'show' CREATE = 'create' UPDATE = 'update' DELETE = 'delete' def __init__(self, plugin, collection, resource, attr_info, allow_bulk=False, member_actions=None, parent=None): if member_actions is None: member_actions = [] self._plugin = plugin self._collection = collection.replace('-', '_') self._resource = resource.replace('-', '_') self._attr_info = attr_info self._allow_bulk = allow_bulk self._native_bulk = self._is_native_bulk_supported() self._policy_attrs = [name for (name, info) in self._attr_info.items() if info.get('required_by_policy')] self._publisher_id = notifier_api.publisher_id('network') self._member_actions = member_actions if parent: self._parent_id_name = '%s_id' % parent['member_name'] parent_part = '_%s' % parent['member_name'] else: self._parent_id_name = None parent_part = '' self._plugin_handlers = { self.LIST: 'get%s_%s' % (parent_part, self._collection), self.SHOW: 'get%s_%s' % (parent_part, self._resource) } for action in [self.CREATE, self.UPDATE, self.DELETE]: self._plugin_handlers[action] = '%s%s_%s' % (action, parent_part, self._resource) def _is_native_bulk_supported(self): native_bulk_attr_name = ("_%s__native_bulk_support" % self._plugin.__class__.__name__) return getattr(self._plugin, native_bulk_attr_name, False) def _is_visible(self, attr): attr_val = self._attr_info.get(attr) return attr_val and attr_val['is_visible'] def _view(self, data, fields_to_strip=None): # make sure fields_to_strip is iterable if not fields_to_strip: fields_to_strip = [] return dict(item for item in data.iteritems() if (self._is_visible(item[0]) and item[0] not in fields_to_strip)) def _do_field_list(self, original_fields): fields_to_add = None # don't do anything if fields were not specified in the request if original_fields: fields_to_add = [attr for attr in self._policy_attrs if attr not in original_fields] original_fields.extend(self._policy_attrs) return original_fields, fields_to_add def __getattr__(self, name): if name in self._member_actions: def _handle_action(request, id, **kwargs): if 'body' in kwargs: body = kwargs.pop('body') return getattr(self._plugin, name)(request.context, id, body, **kwargs) else: return getattr(self._plugin, name)(request.context, id, **kwargs) return _handle_action else: raise AttributeError def _items(self, request, do_authz=False, parent_id=None): """Retrieves and formats a list of elements of the requested entity""" # NOTE(salvatore-orlando): The following ensures that fields which # are needed for authZ policy validation are not stripped away by the # plugin before returning. original_fields, fields_to_add = self._do_field_list(_fields(request)) kwargs = {'filters': _filters(request, self._attr_info), 'fields': original_fields} if parent_id: kwargs[self._parent_id_name] = parent_id obj_getter = getattr(self._plugin, self._plugin_handlers[self.LIST]) obj_list = obj_getter(request.context, **kwargs) # Check authz if do_authz: # FIXME(salvatore-orlando): obj_getter might return references to # other resources. Must check authZ on them too. # Omit items from list that should not be visible obj_list = [obj for obj in obj_list if policy.check(request.context, self._plugin_handlers[self.SHOW], obj, plugin=self._plugin)] return {self._collection: [self._view(obj, fields_to_strip=fields_to_add) for obj in obj_list]} def _item(self, request, id, do_authz=False, field_list=None, parent_id=None): """Retrieves and formats a single element of the requested entity""" kwargs = {'fields': field_list} action = self._plugin_handlers[self.SHOW] if parent_id: kwargs[self._parent_id_name] = parent_id obj_getter = getattr(self._plugin, action) obj = obj_getter(request.context, id, **kwargs) # Check authz # FIXME(salvatore-orlando): obj_getter might return references to # other resources. Must check authZ on them too. if do_authz: policy.enforce(request.context, action, obj, plugin=self._plugin) return obj def index(self, request, **kwargs): """Returns a list of the requested entity""" parent_id = kwargs.get(self._parent_id_name) return self._items(request, True, parent_id) def show(self, request, id, **kwargs): """Returns detailed information about the requested entity""" try: # NOTE(salvatore-orlando): The following ensures that fields # which are needed for authZ policy validation are not stripped # away by the plugin before returning. field_list, added_fields = self._do_field_list(_fields(request)) parent_id = kwargs.get(self._parent_id_name) return {self._resource: self._view(self._item(request, id, do_authz=True, field_list=field_list, parent_id=parent_id), fields_to_strip=added_fields)} except exceptions.PolicyNotAuthorized: # To avoid giving away information, pretend that it # doesn't exist raise webob.exc.HTTPNotFound() def _emulate_bulk_create(self, obj_creator, request, body, parent_id=None): objs = [] try: for item in body[self._collection]: kwargs = {self._resource: item} if parent_id: kwargs[self._parent_id_name] = parent_id objs.append(self._view(obj_creator(request.context, **kwargs))) return objs # Note(salvatore-orlando): broad catch as in theory a plugin # could raise any kind of exception except Exception as ex: for obj in objs: obj_deleter = getattr(self._plugin, self._plugin_handlers[self.DELETE]) try: kwargs = ({self._parent_id_name: parent_id} if parent_id else {}) obj_deleter(request.context, obj['id'], **kwargs) except Exception: # broad catch as our only purpose is to log the exception LOG.exception(_("Unable to undo add for " "%(resource)s %(id)s"), {'resource': self._resource, 'id': obj['id']}) # TODO(salvatore-orlando): The object being processed when the # plugin raised might have been created or not in the db. # We need a way for ensuring that if it has been created, # it is then deleted raise ex def create(self, request, body=None, **kwargs): """Creates a new instance of the requested entity""" parent_id = kwargs.get(self._parent_id_name) notifier_api.notify(request.context, self._publisher_id, self._resource + '.create.start', notifier_api.CONF.default_notification_level, body) body = Controller.prepare_request_body(request.context, body, True, self._resource, self._attr_info, allow_bulk=self._allow_bulk) action = self._plugin_handlers[self.CREATE] # Check authz if self._collection in body: # Have to account for bulk create items = body[self._collection] deltas = {} bulk = True else: items = [body] bulk = False for item in items: self._validate_network_tenant_ownership(request, item[self._resource]) policy.enforce(request.context, action, item[self._resource], plugin=self._plugin) try: tenant_id = item[self._resource]['tenant_id'] count = quota.QUOTAS.count(request.context, self._resource, self._plugin, self._collection, tenant_id) if bulk: delta = deltas.get(tenant_id, 0) + 1 deltas[tenant_id] = delta else: delta = 1 kwargs = {self._resource: count + delta} except exceptions.QuotaResourceUnknown as e: # We don't want to quota this resource LOG.debug(e) else: quota.QUOTAS.limit_check(request.context, item[self._resource]['tenant_id'], **kwargs) def notify(create_result): notifier_api.notify(request.context, self._publisher_id, self._resource + '.create.end', notifier_api.CONF.default_notification_level, create_result) return create_result kwargs = {self._parent_id_name: parent_id} if parent_id else {} if self._collection in body and self._native_bulk: # plugin does atomic bulk create operations obj_creator = getattr(self._plugin, "%s_bulk" % action) objs = obj_creator(request.context, body, **kwargs) return notify({self._collection: [self._view(obj) for obj in objs]}) else: obj_creator = getattr(self._plugin, action) if self._collection in body: # Emulate atomic bulk behavior objs = self._emulate_bulk_create(obj_creator, request, body, parent_id) return notify({self._collection: objs}) else: kwargs.update({self._resource: body}) obj = obj_creator(request.context, **kwargs) return notify({self._resource: self._view(obj)}) def delete(self, request, id, **kwargs): """Deletes the specified entity""" notifier_api.notify(request.context, self._publisher_id, self._resource + '.delete.start', notifier_api.CONF.default_notification_level, {self._resource + '_id': id}) action = self._plugin_handlers[self.DELETE] # Check authz parent_id = kwargs.get(self._parent_id_name) obj = self._item(request, id, parent_id=parent_id) try: policy.enforce(request.context, action, obj, plugin=self._plugin) 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, **kwargs) notifier_api.notify(request.context, self._publisher_id, self._resource + '.delete.end', notifier_api.CONF.default_notification_level, {self._resource + '_id': id}) def update(self, request, id, body=None, **kwargs): """Updates the specified entity's attributes""" parent_id = kwargs.get(self._parent_id_name) payload = body.copy() payload['id'] = id notifier_api.notify(request.context, self._publisher_id, self._resource + '.update.start', notifier_api.CONF.default_notification_level, payload) body = Controller.prepare_request_body(request.context, body, False, self._resource, self._attr_info, allow_bulk=self._allow_bulk) action = self._plugin_handlers[self.UPDATE] # Load object to check authz # but pass only attributes in the original body and required # by the policy engine to the policy 'brain' field_list = [name for (name, value) in self._attr_info.iteritems() if ('required_by_policy' in value and value['required_by_policy'] or 'default' not in value)] orig_obj = self._item(request, id, field_list=field_list, parent_id=parent_id) orig_obj.update(body[self._resource]) try: policy.enforce(request.context, action, orig_obj, plugin=self._plugin) 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} if parent_id: kwargs[self._parent_id_name] = parent_id obj = obj_updater(request.context, id, **kwargs) result = {self._resource: self._view(obj)} notifier_api.notify(request.context, self._publisher_id, self._resource + '.update.end', notifier_api.CONF.default_notification_level, result) return result @staticmethod def _populate_tenant_id(context, res_dict, is_create): if (('tenant_id' in res_dict and res_dict['tenant_id'] != context.tenant_id and not context.is_admin)): msg = _("Specifying 'tenant_id' other than authenticated " "tenant in request requires admin privileges") raise webob.exc.HTTPBadRequest(msg) if is_create and 'tenant_id' not in res_dict: if context.tenant_id: res_dict['tenant_id'] = context.tenant_id else: msg = _("Running without keystyone AuthN requires " " that tenant_id is specified") raise webob.exc.HTTPBadRequest(msg) @staticmethod def prepare_request_body(context, body, is_create, resource, attr_info, allow_bulk=False): """ verifies required attributes are in request body, and that an attribute is only specified if it is allowed for the given operation (create/update). Attribute with default values are considered to be optional. body argument must be the deserialized body """ collection = resource + "s" if not body: raise webob.exc.HTTPBadRequest(_("Resource body required")) prep_req_body = lambda x: Controller.prepare_request_body( context, x if resource in x else {resource: x}, is_create, resource, attr_info, allow_bulk) if collection in body: if not allow_bulk: raise webob.exc.HTTPBadRequest(_("Bulk operation " "not supported")) bulk_body = [prep_req_body(item) for item in body[collection]] if not bulk_body: raise webob.exc.HTTPBadRequest(_("Resources required")) return {collection: bulk_body} res_dict = body.get(resource) if res_dict is None: msg = _("Unable to find '%s' in request body") % resource raise webob.exc.HTTPBadRequest(msg) Controller._populate_tenant_id(context, res_dict, is_create) if is_create: # POST for attr, attr_vals in attr_info.iteritems(): if attr_vals['allow_post']: if ('default' not in attr_vals and attr not in res_dict): msg = _("Failed to parse request. Required " "attribute '%s' not specified") % attr raise webob.exc.HTTPBadRequest(msg) res_dict[attr] = res_dict.get(attr, attr_vals.get('default')) else: if attr in res_dict: msg = _("Attribute '%s' not allowed in POST") % attr raise webob.exc.HTTPBadRequest(msg) else: # PUT for attr, attr_vals in attr_info.iteritems(): if attr in res_dict and not attr_vals['allow_put']: msg = _("Cannot update read-only attribute %s") % attr raise webob.exc.HTTPBadRequest(msg) for attr, attr_vals in attr_info.iteritems(): if (attr not in res_dict or res_dict[attr] is attributes.ATTR_NOT_SPECIFIED): continue # Convert values if necessary if 'convert_to' in attr_vals: res_dict[attr] = attr_vals['convert_to'](res_dict[attr]) # Check that configured values are correct if 'validate' not in attr_vals: continue for rule in attr_vals['validate']: res = attributes.validators[rule](res_dict[attr], attr_vals['validate'][rule]) if res: msg_dict = dict(attr=attr, reason=res) msg = _("Invalid input for %(attr)s. " "Reason: %(reason)s.") % msg_dict raise webob.exc.HTTPBadRequest(msg) return body def _validate_network_tenant_ownership(self, request, resource_item): # TODO(salvatore-orlando): consider whether this check can be folded # in the policy engine if self._resource not in ('port', 'subnet'): return network = self._plugin.get_network( request.context, resource_item['network_id']) # do not perform the check on shared networks if network.get('shared'): return network_owner = network['tenant_id'] if network_owner != resource_item['tenant_id']: msg = _("Tenant %(tenant_id)s not allowed to " "create %(resource)s on this network") raise webob.exc.HTTPForbidden(msg % { "tenant_id": resource_item['tenant_id'], "resource": self._resource, }) def create_resource(collection, resource, plugin, params, allow_bulk=False, member_actions=None, parent=None): controller = Controller(plugin, collection, resource, params, allow_bulk, member_actions=member_actions, parent=parent) return wsgi_resource.Resource(controller, FAULT_MAP)