Merge "Implement filter validation"
This commit is contained in:
commit
ca13e651c9
@ -62,13 +62,21 @@ def check_request_for_revision_constraint(request):
|
||||
return revision_number
|
||||
|
||||
|
||||
def get_filters(request, attr_info, skips=None):
|
||||
def is_filter_validation_enabled():
|
||||
return 'filter-validation' in (extensions.PluginAwareExtensionManager.
|
||||
get_instance().extensions)
|
||||
|
||||
|
||||
def get_filters(request, attr_info, skips=None,
|
||||
is_filter_validation_supported=False):
|
||||
return get_filters_from_dict(request.GET.dict_of_lists(),
|
||||
attr_info,
|
||||
skips)
|
||||
skips,
|
||||
is_filter_validation_supported)
|
||||
|
||||
|
||||
def get_filters_from_dict(data, attr_info, skips=None):
|
||||
def get_filters_from_dict(data, attr_info, skips=None,
|
||||
is_filter_validation_supported=False):
|
||||
"""Extracts the filters from a dict of query parameters.
|
||||
|
||||
Returns a dict of lists for the filters:
|
||||
@ -80,12 +88,19 @@ def get_filters_from_dict(data, attr_info, skips=None):
|
||||
is_empty_string_supported = is_empty_string_filtering_supported()
|
||||
skips = skips or []
|
||||
res = {}
|
||||
invalid_keys = []
|
||||
check_is_filter = False
|
||||
if is_filter_validation_supported and is_filter_validation_enabled():
|
||||
check_is_filter = True
|
||||
for key, values in data.items():
|
||||
if key in skips or hasattr(model_base.BASEV2, key):
|
||||
continue
|
||||
values = [v for v in values
|
||||
if v or (v == "" and is_empty_string_supported)]
|
||||
key_attr_info = attr_info.get(key, {})
|
||||
if check_is_filter and not key_attr_info.get('is_filter'):
|
||||
invalid_keys.append(key)
|
||||
continue
|
||||
if 'convert_list_to' in key_attr_info:
|
||||
values = key_attr_info['convert_list_to'](values)
|
||||
elif 'convert_to' in key_attr_info:
|
||||
@ -93,6 +108,9 @@ def get_filters_from_dict(data, attr_info, skips=None):
|
||||
values = [convert_to(v) for v in values]
|
||||
if values:
|
||||
res[key] = values
|
||||
if invalid_keys:
|
||||
msg = _("%s is invalid attribute for filtering") % invalid_keys
|
||||
raise exc.HTTPBadRequest(explanation=msg)
|
||||
return res
|
||||
|
||||
|
||||
@ -248,6 +266,12 @@ def is_native_sorting_supported(plugin):
|
||||
return getattr(plugin, native_sorting_attr_name, False)
|
||||
|
||||
|
||||
def is_filter_validation_supported(plugin):
|
||||
filter_validation_attr_name = ("_%s__filter_validation_support"
|
||||
% plugin.__class__.__name__)
|
||||
return getattr(plugin, filter_validation_attr_name, False)
|
||||
|
||||
|
||||
class PaginationHelper(object):
|
||||
|
||||
def __init__(self, request, primary_key='id'):
|
||||
|
@ -105,6 +105,7 @@ class Controller(object):
|
||||
self._native_bulk = self._is_native_bulk_supported()
|
||||
self._native_pagination = self._is_native_pagination_supported()
|
||||
self._native_sorting = self._is_native_sorting_supported()
|
||||
self._filter_validation = self._is_filter_validation_supported()
|
||||
self._policy_attrs = self._init_policy_attrs()
|
||||
self._notifier = n_rpc.get_notifier('network')
|
||||
self._member_actions = member_actions
|
||||
@ -151,6 +152,9 @@ class Controller(object):
|
||||
def _is_native_sorting_supported(self):
|
||||
return api_common.is_native_sorting_supported(self._plugin)
|
||||
|
||||
def _is_filter_validation_supported(self):
|
||||
return api_common.is_filter_validation_supported(self._plugin)
|
||||
|
||||
def _exclude_attributes_by_policy(self, context, data):
|
||||
"""Identifies attributes to exclude according to authZ policies.
|
||||
|
||||
@ -282,9 +286,11 @@ class Controller(object):
|
||||
# plugin before returning.
|
||||
original_fields, fields_to_add = self._do_field_list(
|
||||
api_common.list_args(request, 'fields'))
|
||||
filters = api_common.get_filters(request, self._attr_info,
|
||||
['fields', 'sort_key', 'sort_dir',
|
||||
'limit', 'marker', 'page_reverse'])
|
||||
filters = api_common.get_filters(
|
||||
request, self._attr_info,
|
||||
['fields', 'sort_key', 'sort_dir',
|
||||
'limit', 'marker', 'page_reverse'],
|
||||
is_filter_validation_supported=self._filter_validation)
|
||||
kwargs = {'filters': filters,
|
||||
'fields': original_fields}
|
||||
sorting_helper = self._get_sorting_helper(request)
|
||||
|
@ -44,6 +44,7 @@ import six
|
||||
|
||||
import neutron
|
||||
from neutron._i18n import _
|
||||
from neutron.api import api_common
|
||||
from neutron.common import exceptions
|
||||
from neutron.db import api as db_api
|
||||
|
||||
@ -818,3 +819,11 @@ def get_port_binding_by_status_and_host(bindings, status, host='',
|
||||
return binding
|
||||
if raise_if_not_found:
|
||||
raise exceptions.PortBindingNotFound(port_id=port_id, host=host)
|
||||
|
||||
|
||||
def disable_extension_by_service_plugin(core_plugin, service_plugin):
|
||||
if ('filter-validation' in core_plugin.supported_extension_aliases and
|
||||
not api_common.is_filter_validation_supported(service_plugin)):
|
||||
core_plugin.supported_extension_aliases.remove('filter-validation')
|
||||
LOG.info('Disable filter validation extension by service plugin '
|
||||
'%s.', service_plugin.__class__.__name__)
|
||||
|
@ -117,6 +117,11 @@ core_opts = [
|
||||
cfg.BoolOpt('vlan_transparent', default=False,
|
||||
help=_('If True, then allow plugins that support it to '
|
||||
'create VLAN transparent networks.')),
|
||||
cfg.BoolOpt('filter_validation', default=True,
|
||||
help=_('If True, then allow plugins to decide '
|
||||
'whether to perform validations on filter parameters. '
|
||||
'Filter validation is enabled if this config'
|
||||
'is turned on and it is supported by all plugins')),
|
||||
cfg.IntOpt('global_physnet_mtu', default=constants.DEFAULT_NETWORK_MTU,
|
||||
deprecated_name='segment_mtu', deprecated_group='ml2',
|
||||
help=_('MTU of the underlying physical network. Neutron uses '
|
||||
|
@ -140,6 +140,10 @@ class NeutronDbPluginV2(db_base_plugin_common.DbBasePluginCommon,
|
||||
__native_bulk_support = True
|
||||
__native_pagination_support = True
|
||||
__native_sorting_support = True
|
||||
# This attribute specifies whether the plugin supports or not
|
||||
# filter validations. Name mangling is used in
|
||||
# order to ensure it is qualified by class
|
||||
__filter_validation_support = False
|
||||
|
||||
def has_native_datastore(self):
|
||||
return True
|
||||
|
30
neutron/extensions/_filter_validation_lib.py
Normal file
30
neutron/extensions/_filter_validation_lib.py
Normal file
@ -0,0 +1,30 @@
|
||||
# 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.
|
||||
|
||||
"""
|
||||
This module should be deleted once neutron-lib containing
|
||||
https://review.openstack.org/#/c/580190/ change is released.
|
||||
"""
|
||||
|
||||
|
||||
ALIAS = 'filter-validation'
|
||||
IS_SHIM_EXTENSION = True
|
||||
IS_STANDARD_ATTR_EXTENSION = False
|
||||
NAME = 'Filter parameters validation'
|
||||
DESCRIPTION = 'Provides validation on filter parameters.'
|
||||
UPDATED_TIMESTAMP = '2018-03-21T10:00:00-00:00'
|
||||
RESOURCE_ATTRIBUTE_MAP = {}
|
||||
SUB_RESOURCE_ATTRIBUTE_MAP = {}
|
||||
ACTION_MAP = {}
|
||||
REQUIRED_EXTENSIONS = []
|
||||
OPTIONAL_EXTENSIONS = []
|
||||
ACTION_STATUS = {}
|
@ -46,6 +46,8 @@ RESOURCE_ATTRIBUTE_MAP = {
|
||||
'convert_to': convert_to_mac_if_none,
|
||||
'validate': {'type:mac_address': None},
|
||||
'enforce_policy': True,
|
||||
'is_filter': True,
|
||||
'is_sort_key': True,
|
||||
'is_visible': True},
|
||||
}
|
||||
}
|
||||
|
35
neutron/extensions/filter_validation.py
Normal file
35
neutron/extensions/filter_validation.py
Normal file
@ -0,0 +1,35 @@
|
||||
# Copyright (c) 2017 Huawei Technology, 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.
|
||||
|
||||
from neutron_lib.api import extensions
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log as logging
|
||||
|
||||
from neutron.extensions import _filter_validation_lib as apidef
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _disable_extension_by_config(aliases):
|
||||
if not cfg.CONF.filter_validation:
|
||||
if 'filter-validation' in aliases:
|
||||
aliases.remove('filter-validation')
|
||||
LOG.info('Disabled filter validation extension.')
|
||||
|
||||
|
||||
class Filter_validation(extensions.APIExtensionDescriptor):
|
||||
"""Extension class supporting filter validation."""
|
||||
|
||||
api_definition = apidef
|
@ -30,6 +30,9 @@ class Network_ip_availability(api_extensions.APIExtensionDescriptor):
|
||||
"""Returns Extended Resource for service type management."""
|
||||
resource_attributes = apidef.RESOURCE_ATTRIBUTE_MAP[
|
||||
apidef.RESOURCE_PLURAL]
|
||||
# TODO(hongbin): Delete _populate_is_filter_keyword once neutron-lib
|
||||
# containing https://review.openstack.org/#/c/583838/ is released.
|
||||
cls._populate_is_filter_keyword(resource_attributes)
|
||||
controller = base.create_resource(
|
||||
apidef.RESOURCE_PLURAL,
|
||||
apidef.RESOURCE_NAME,
|
||||
@ -38,3 +41,13 @@ class Network_ip_availability(api_extensions.APIExtensionDescriptor):
|
||||
return [extensions.ResourceExtension(apidef.COLLECTION_NAME,
|
||||
controller,
|
||||
attr_map=resource_attributes)]
|
||||
|
||||
@classmethod
|
||||
def _populate_is_filter_keyword(cls, params):
|
||||
filter_keys = ['network_id', 'network_name', 'tenant_id',
|
||||
'project_id']
|
||||
for name in params:
|
||||
if name in filter_keys:
|
||||
params[name]['is_filter'] = True
|
||||
params['ip_version'] = {'allow_post': False, 'allow_put': False,
|
||||
'is_visible': False, 'is_filter': True}
|
||||
|
@ -52,6 +52,9 @@ class Portbindings_extended(api_extensions.ExtensionDescriptor):
|
||||
|
||||
params = pbe_ext.SUB_RESOURCE_ATTRIBUTE_MAP[
|
||||
pbe_ext.COLLECTION_NAME]['parameters']
|
||||
# TODO(hongbin): Delete _populate_is_filter_keyword once neutron-lib
|
||||
# containing https://review.openstack.org/#/c/583437/ is released.
|
||||
cls._populate_is_filter_keyword(params)
|
||||
parent = pbe_ext.SUB_RESOURCE_ATTRIBUTE_MAP[
|
||||
pbe_ext.COLLECTION_NAME]['parent']
|
||||
controller = base.create_resource(
|
||||
@ -75,3 +78,11 @@ class Portbindings_extended(api_extensions.ExtensionDescriptor):
|
||||
]
|
||||
|
||||
return exts
|
||||
|
||||
@classmethod
|
||||
def _populate_is_filter_keyword(cls, params):
|
||||
filter_keys = [pbe_ext.HOST, pbe_ext.VIF_TYPE, pbe_ext.VNIC_TYPE,
|
||||
pbe_ext.STATUS]
|
||||
for name in params:
|
||||
if name in filter_keys:
|
||||
params[name]['is_filter'] = True
|
||||
|
@ -54,22 +54,27 @@ RESOURCE_ATTRIBUTE_MAP = {
|
||||
RESOURCE_COLLECTION: {
|
||||
'id': {'allow_post': False, 'allow_put': False,
|
||||
'validate': {'type:uuid': None},
|
||||
'is_visible': True, 'primary_key': True},
|
||||
'is_visible': True, 'primary_key': True,
|
||||
'is_filter': True},
|
||||
'object_type': {'allow_post': True, 'allow_put': False,
|
||||
'convert_to': convert_valid_object_type,
|
||||
'is_visible': True, 'default': None,
|
||||
'is_filter': True,
|
||||
'enforce_policy': True},
|
||||
'object_id': {'allow_post': True, 'allow_put': False,
|
||||
'validate': {'type:uuid': None},
|
||||
'is_visible': True, 'enforce_policy': True},
|
||||
'is_visible': True, 'enforce_policy': True,
|
||||
'is_filter': True},
|
||||
'target_tenant': {'allow_post': True, 'allow_put': True,
|
||||
'validate': {
|
||||
'type:string': db_const.PROJECT_ID_FIELD_SIZE},
|
||||
'is_visible': True, 'enforce_policy': True},
|
||||
'is_visible': True, 'enforce_policy': True,
|
||||
'is_filter': True},
|
||||
'tenant_id': {'allow_post': True, 'allow_put': False,
|
||||
'validate': {
|
||||
'type:string': db_const.PROJECT_ID_FIELD_SIZE},
|
||||
'required_by_policy': True, 'is_visible': True},
|
||||
'required_by_policy': True, 'is_visible': True,
|
||||
'is_filter': True},
|
||||
'action': {'allow_post': True, 'allow_put': False,
|
||||
# action depends on type so validation has to occur in
|
||||
# the extension
|
||||
@ -77,7 +82,8 @@ RESOURCE_ATTRIBUTE_MAP = {
|
||||
'type:string': db_const.DESCRIPTION_FIELD_SIZE},
|
||||
# we set enforce_policy so operators can define policies
|
||||
# that restrict actions
|
||||
'is_visible': True, 'enforce_policy': True}
|
||||
'is_visible': True, 'enforce_policy': True,
|
||||
'is_filter': True}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -221,16 +221,17 @@ RESOURCE_ATTRIBUTE_MAP = {
|
||||
'id': {'allow_post': False, 'allow_put': False,
|
||||
'validate': {'type:uuid': None},
|
||||
'is_visible': True,
|
||||
'is_filter': True,
|
||||
'primary_key': True},
|
||||
'name': {'allow_post': True, 'allow_put': True,
|
||||
'is_visible': True, 'default': '',
|
||||
'is_visible': True, 'default': '', 'is_filter': True,
|
||||
'validate': {
|
||||
'type:name_not_default': db_const.NAME_FIELD_SIZE}},
|
||||
'tenant_id': {'allow_post': True, 'allow_put': False,
|
||||
'required_by_policy': True,
|
||||
'validate': {
|
||||
'type:string': db_const.PROJECT_ID_FIELD_SIZE},
|
||||
'is_visible': True},
|
||||
'is_visible': True, 'is_filter': True},
|
||||
SECURITYGROUPRULES: {'allow_post': False, 'allow_put': False,
|
||||
'is_visible': True},
|
||||
},
|
||||
@ -238,35 +239,43 @@ RESOURCE_ATTRIBUTE_MAP = {
|
||||
'id': {'allow_post': False, 'allow_put': False,
|
||||
'validate': {'type:uuid': None},
|
||||
'is_visible': True,
|
||||
'is_filter': True,
|
||||
'primary_key': True},
|
||||
'security_group_id': {'allow_post': True, 'allow_put': False,
|
||||
'is_visible': True, 'required_by_policy': True},
|
||||
'is_visible': True, 'required_by_policy': True,
|
||||
'is_filter': True},
|
||||
'remote_group_id': {'allow_post': True, 'allow_put': False,
|
||||
'default': None, 'is_visible': True},
|
||||
'default': None, 'is_visible': True,
|
||||
'is_filter': True},
|
||||
'direction': {'allow_post': True, 'allow_put': False,
|
||||
'is_visible': True,
|
||||
'is_visible': True, 'is_filter': True,
|
||||
'validate': {'type:values': ['ingress', 'egress']}},
|
||||
'protocol': {'allow_post': True, 'allow_put': False,
|
||||
'is_visible': True, 'default': None,
|
||||
'is_filter': True,
|
||||
'convert_to': convert_protocol},
|
||||
'port_range_min': {'allow_post': True, 'allow_put': False,
|
||||
'convert_to': convert_validate_port_value,
|
||||
'default': None, 'is_visible': True},
|
||||
'default': None, 'is_visible': True,
|
||||
'is_filter': True},
|
||||
'port_range_max': {'allow_post': True, 'allow_put': False,
|
||||
'convert_to': convert_validate_port_value,
|
||||
'default': None, 'is_visible': True},
|
||||
'default': None, 'is_visible': True,
|
||||
'is_filter': True},
|
||||
'ethertype': {'allow_post': True, 'allow_put': False,
|
||||
'is_visible': True, 'default': 'IPv4',
|
||||
'is_filter': True,
|
||||
'convert_to': convert_ethertype_to_case_insensitive,
|
||||
'validate': {'type:values': sg_supported_ethertypes}},
|
||||
'remote_ip_prefix': {'allow_post': True, 'allow_put': False,
|
||||
'default': None, 'is_visible': True,
|
||||
'is_filter': True,
|
||||
'convert_to': convert_ip_prefix_to_cidr},
|
||||
'tenant_id': {'allow_post': True, 'allow_put': False,
|
||||
'required_by_policy': True,
|
||||
'validate': {
|
||||
'type:string': db_const.PROJECT_ID_FIELD_SIZE},
|
||||
'is_visible': True},
|
||||
'is_visible': True, 'is_filter': True},
|
||||
}
|
||||
}
|
||||
|
||||
@ -275,6 +284,7 @@ EXTENDED_ATTRIBUTES_2_0 = {
|
||||
'ports': {SECURITYGROUPS: {'allow_post': True,
|
||||
'allow_put': True,
|
||||
'is_visible': True,
|
||||
'is_filter': True,
|
||||
'convert_to':
|
||||
converters.convert_none_to_empty_list,
|
||||
'validate': {'type:uuid_list': None},
|
||||
|
@ -44,6 +44,7 @@ RESOURCE_ATTRIBUTE_MAP = {
|
||||
'id': {'allow_post': False,
|
||||
'allow_put': False,
|
||||
'validate': {'type:uuid': None},
|
||||
'is_filter': True,
|
||||
'is_visible': True,
|
||||
'primary_key': True},
|
||||
'tenant_id': {'allow_post': True,
|
||||
@ -54,17 +55,20 @@ RESOURCE_ATTRIBUTE_MAP = {
|
||||
'network_id': {'allow_post': True,
|
||||
'allow_put': False,
|
||||
'validate': {'type:uuid': None},
|
||||
'is_filter': True,
|
||||
'is_visible': True},
|
||||
PHYSICAL_NETWORK: {'allow_post': True,
|
||||
'allow_put': False,
|
||||
'default': constants.ATTR_NOT_SPECIFIED,
|
||||
'validate': {'type:string':
|
||||
providernet.PHYSICAL_NETWORK_MAX_LEN},
|
||||
'is_filter': True,
|
||||
'is_visible': True},
|
||||
NETWORK_TYPE: {'allow_post': True,
|
||||
'allow_put': False,
|
||||
'validate': {'type:string':
|
||||
providernet.NETWORK_TYPE_MAX_LEN},
|
||||
'is_filter': True,
|
||||
'is_visible': True},
|
||||
SEGMENTATION_ID: {'allow_post': True,
|
||||
'allow_put': False,
|
||||
@ -75,6 +79,7 @@ RESOURCE_ATTRIBUTE_MAP = {
|
||||
'allow_put': True,
|
||||
'default': constants.ATTR_NOT_SPECIFIED,
|
||||
'validate': {'type:string_or_none': NAME_LEN},
|
||||
'is_filter': True,
|
||||
'is_visible': True}
|
||||
},
|
||||
subnet_def.COLLECTION_NAME: {
|
||||
@ -82,6 +87,7 @@ RESOURCE_ATTRIBUTE_MAP = {
|
||||
'allow_put': False,
|
||||
'default': None,
|
||||
'validate': {'type:uuid_or_none': None},
|
||||
'is_filter': True,
|
||||
'is_visible': True, },
|
||||
},
|
||||
}
|
||||
|
@ -27,6 +27,7 @@ from osprofiler import profiler
|
||||
import six
|
||||
|
||||
from neutron._i18n import _
|
||||
from neutron.common import utils
|
||||
from neutron.plugins.common import constants
|
||||
|
||||
|
||||
@ -221,6 +222,9 @@ class NeutronManager(object):
|
||||
hasattr(plugin_inst, 'agent_notifiers')):
|
||||
plugin.agent_notifiers.update(plugin_inst.agent_notifiers)
|
||||
|
||||
# disable incompatible extensions in core plugin if any
|
||||
utils.disable_extension_by_service_plugin(plugin, plugin_inst)
|
||||
|
||||
LOG.debug("Successfully loaded %(type)s plugin. "
|
||||
"Description: %(desc)s",
|
||||
{"type": plugin_type,
|
||||
|
@ -167,6 +167,8 @@ class NeutronPecanController(object):
|
||||
raise exceptions.Invalid(
|
||||
_("Native pagination depends on native sorting")
|
||||
)
|
||||
self.filter_validation = api_common.is_filter_validation_supported(
|
||||
self.plugin)
|
||||
self.primary_key = self._get_primary_key()
|
||||
|
||||
self.parent = parent_resource
|
||||
|
@ -77,7 +77,8 @@ def _set_filters(state, controller):
|
||||
{k: _listify(v) for k, v in params.items()},
|
||||
controller.resource_info,
|
||||
skips=['fields', 'sort_key', 'sort_dir',
|
||||
'limit', 'marker', 'page_reverse'])
|
||||
'limit', 'marker', 'page_reverse'],
|
||||
is_filter_validation_supported=controller.filter_validation)
|
||||
return filters
|
||||
|
||||
|
||||
|
@ -87,6 +87,7 @@ from neutron.db import securitygroups_rpc_base as sg_db_rpc
|
||||
from neutron.db import segments_db
|
||||
from neutron.db import subnet_service_type_mixin
|
||||
from neutron.db import vlantransparent_db
|
||||
from neutron.extensions import filter_validation
|
||||
from neutron.extensions import providernet as provider
|
||||
from neutron.extensions import vlantransparent
|
||||
from neutron.objects import base as base_obj
|
||||
@ -148,6 +149,10 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2,
|
||||
__native_bulk_support = True
|
||||
__native_pagination_support = True
|
||||
__native_sorting_support = True
|
||||
# This attribute specifies whether the plugin supports or not
|
||||
# filter validations. Name mangling is used in
|
||||
# order to ensure it is qualified by class
|
||||
__filter_validation_support = True
|
||||
|
||||
# List of supported extensions
|
||||
_supported_extension_aliases = ["provider", "external-net", "binding",
|
||||
@ -166,6 +171,7 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2,
|
||||
"ip-substring-filtering",
|
||||
"port-security-groups-filtering",
|
||||
"empty-string-filtering",
|
||||
"filter-validation",
|
||||
"port-mac-address-regenerate",
|
||||
"binding-extended"]
|
||||
|
||||
@ -176,6 +182,7 @@ class Ml2Plugin(db_base_plugin_v2.NeutronDbPluginV2,
|
||||
aliases += self.extension_manager.extension_aliases()
|
||||
sg_rpc.disable_security_group_extension_by_config(aliases)
|
||||
vlantransparent._disable_extension_by_config(aliases)
|
||||
filter_validation._disable_extension_by_config(aliases)
|
||||
self._aliases = aliases
|
||||
return self._aliases
|
||||
|
||||
|
@ -24,6 +24,8 @@ class Plugin(db.AutoAllocatedTopologyMixin):
|
||||
|
||||
supported_extension_aliases = ["auto-allocated-topology"]
|
||||
|
||||
__filter_validation_support = True
|
||||
|
||||
@classmethod
|
||||
def get_instance(cls):
|
||||
if cls._instance is None:
|
||||
|
@ -25,6 +25,8 @@ class FlavorsPlugin(service_base.ServicePluginBase,
|
||||
|
||||
supported_extension_aliases = ['flavors', 'service-type']
|
||||
|
||||
__filter_validation_support = True
|
||||
|
||||
@classmethod
|
||||
def get_plugin_type(cls):
|
||||
return constants.FLAVORS
|
||||
|
@ -88,6 +88,7 @@ class L3RouterPlugin(service_base.ServicePluginBase,
|
||||
|
||||
__native_pagination_support = True
|
||||
__native_sorting_support = True
|
||||
__filter_validation_support = True
|
||||
|
||||
@resource_registry.tracked_resources(router=l3_models.Router,
|
||||
floatingip=l3_models.FloatingIP)
|
||||
|
@ -31,6 +31,7 @@ class LoggingPlugin(log_ext.LoggingPluginBase):
|
||||
|
||||
__native_pagination_support = True
|
||||
__native_sorting_support = True
|
||||
__filter_validation_support = True
|
||||
|
||||
def __init__(self):
|
||||
super(LoggingPlugin, self).__init__()
|
||||
|
@ -26,6 +26,7 @@ class MeteringPlugin(metering_db.MeteringDbMixin):
|
||||
"""Implementation of the Neutron Metering Service Plugin."""
|
||||
supported_extension_aliases = [metering_apidef.ALIAS]
|
||||
path_prefix = "/metering"
|
||||
__filter_validation_support = True
|
||||
|
||||
def __init__(self):
|
||||
super(MeteringPlugin, self).__init__()
|
||||
|
@ -27,6 +27,8 @@ class NetworkIPAvailabilityPlugin(ip_availability_db.IpAvailabilityMixin,
|
||||
|
||||
supported_extension_aliases = ["network-ip-availability"]
|
||||
|
||||
__filter_validation_support = True
|
||||
|
||||
@classmethod
|
||||
def get_instance(cls):
|
||||
if cls._instance is None:
|
||||
|
@ -47,6 +47,7 @@ class QoSPlugin(qos.QoSPluginBase):
|
||||
|
||||
__native_pagination_support = True
|
||||
__native_sorting_support = True
|
||||
__filter_validation_support = True
|
||||
|
||||
def __init__(self):
|
||||
super(QoSPlugin, self).__init__()
|
||||
|
@ -33,6 +33,8 @@ class RevisionPlugin(service_base.ServicePluginBase):
|
||||
supported_extension_aliases = ['standard-attr-revisions',
|
||||
'revision-if-match']
|
||||
|
||||
__filter_validation_support = True
|
||||
|
||||
def __init__(self):
|
||||
super(RevisionPlugin, self).__init__()
|
||||
db_api.sqla_listen(se.Session, 'before_flush', self.bump_revisions)
|
||||
|
@ -68,6 +68,7 @@ class Plugin(db.SegmentDbMixin, segment.SegmentPluginBase):
|
||||
|
||||
__native_pagination_support = True
|
||||
__native_sorting_support = True
|
||||
__filter_validation_support = True
|
||||
|
||||
def __init__(self):
|
||||
self.nova_updater = NovaSegmentNotifier()
|
||||
|
@ -39,6 +39,8 @@ class TagPlugin(common_db_mixin.CommonDbMixin, tagging.TagPluginBase):
|
||||
|
||||
supported_extension_aliases = ['standard-attr-tag']
|
||||
|
||||
__filter_validation_support = True
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
inst = super(TagPlugin, cls).__new__(cls, *args, **kwargs)
|
||||
inst._filter_methods = [] # prevent GC of our partial functions
|
||||
|
@ -25,6 +25,8 @@ class TimeStampPlugin(service_base.ServicePluginBase,
|
||||
|
||||
supported_extension_aliases = ['standard-attr-timestamp']
|
||||
|
||||
__filter_validation_support = True
|
||||
|
||||
def __init__(self):
|
||||
super(TimeStampPlugin, self).__init__()
|
||||
self.register_db_events()
|
||||
|
@ -50,6 +50,7 @@ class TrunkPlugin(service_base.ServicePluginBase,
|
||||
|
||||
__native_pagination_support = True
|
||||
__native_sorting_support = True
|
||||
__filter_validation_support = True
|
||||
|
||||
def __init__(self):
|
||||
self._rpc_backend = None
|
||||
|
@ -17,6 +17,7 @@ NETWORK_API_EXTENSIONS+=",ext-gw-mode"
|
||||
NETWORK_API_EXTENSIONS+=",external-net"
|
||||
NETWORK_API_EXTENSIONS+=",extra_dhcp_opt"
|
||||
NETWORK_API_EXTENSIONS+=",extraroute"
|
||||
NETWORK_API_EXTENSIONS+=",filter-validation"
|
||||
NETWORK_API_EXTENSIONS+=",fip-port-details"
|
||||
NETWORK_API_EXTENSIONS+=",flavors"
|
||||
NETWORK_API_EXTENSIONS+=",ip-substring-filtering"
|
||||
|
@ -86,9 +86,11 @@ class APIv2TestBase(base.BaseTestCase):
|
||||
self._plugin_patcher = mock.patch(plugin, autospec=True)
|
||||
self.plugin = self._plugin_patcher.start()
|
||||
instance = self.plugin.return_value
|
||||
instance.supported_extension_aliases = ['empty-string-filtering']
|
||||
instance.supported_extension_aliases = ['empty-string-filtering',
|
||||
'filter-validation']
|
||||
instance._NeutronPluginBaseV2__native_pagination_support = True
|
||||
instance._NeutronPluginBaseV2__native_sorting_support = True
|
||||
instance._NeutronPluginBaseV2__filter_validation_support = True
|
||||
tools.make_mock_plugin_json_encodable(instance)
|
||||
|
||||
api = router.APIRouter()
|
||||
@ -1612,6 +1614,52 @@ class FiltersTestCase(base.BaseTestCase):
|
||||
'project_id': {'key': 'val'}}
|
||||
self.assertEqual(expect_attr_info, attr_info)
|
||||
|
||||
@mock.patch('neutron.api.api_common.is_filter_validation_enabled',
|
||||
return_value=True)
|
||||
def test_attr_info_with_filter_validation(self, mock_validation_enabled):
|
||||
attr_info = {}
|
||||
self._test_attr_info(attr_info)
|
||||
|
||||
attr_info = {'foo': {}}
|
||||
self._test_attr_info(attr_info)
|
||||
|
||||
attr_info = {'foo': {'is_filter': False}}
|
||||
self._test_attr_info(attr_info)
|
||||
|
||||
attr_info = {'foo': {'is_filter': False}, 'bar': {'is_filter': True},
|
||||
'baz': {'is_filter': True}, 'qux': {'is_filter': True}}
|
||||
self._test_attr_info(attr_info)
|
||||
|
||||
attr_info = {'foo': {'is_filter': True}, 'bar': {'is_filter': True},
|
||||
'baz': {'is_filter': True}, 'qux': {'is_filter': True}}
|
||||
expect_val = {'foo': ['4'], 'bar': ['3'], 'baz': ['2'], 'qux': ['1']}
|
||||
self._test_attr_info(attr_info, expect_val)
|
||||
|
||||
attr_info = {'foo': {'is_filter': True}, 'bar': {'is_filter': True},
|
||||
'baz': {'is_filter': True}, 'qux': {'is_filter': True},
|
||||
'quz': {}}
|
||||
expect_val = {'foo': ['4'], 'bar': ['3'], 'baz': ['2'], 'qux': ['1']}
|
||||
self._test_attr_info(attr_info, expect_val)
|
||||
|
||||
attr_info = {'foo': {'is_filter': True}, 'bar': {'is_filter': True},
|
||||
'baz': {'is_filter': True}, 'qux': {'is_filter': True},
|
||||
'quz': {'is_filter': False}}
|
||||
expect_val = {'foo': ['4'], 'bar': ['3'], 'baz': ['2'], 'qux': ['1']}
|
||||
self._test_attr_info(attr_info, expect_val)
|
||||
|
||||
def _test_attr_info(self, attr_info, expect_val=None):
|
||||
path = '/?foo=4&bar=3&baz=2&qux=1'
|
||||
request = webob.Request.blank(path)
|
||||
if expect_val:
|
||||
actual_val = api_common.get_filters(
|
||||
request, attr_info,
|
||||
is_filter_validation_supported=True)
|
||||
self.assertEqual(expect_val, actual_val)
|
||||
else:
|
||||
self.assertRaises(
|
||||
exc.HTTPBadRequest, api_common.get_filters, request, attr_info,
|
||||
is_filter_validation_supported=True)
|
||||
|
||||
def test_attr_info_without_conversion(self):
|
||||
path = '/?foo=4&bar=3&baz=2&qux=1'
|
||||
request = webob.Request.blank(path)
|
||||
|
@ -158,6 +158,12 @@ class NeutronDbPluginV2TestCase(testlib_api.WebTestCase):
|
||||
|
||||
self._skip_native_pagination = not _is_native_pagination_support()
|
||||
|
||||
def _is_filter_validation_support():
|
||||
return 'filter-validation' in (directory.get_plugin().
|
||||
supported_extension_aliases)
|
||||
|
||||
self._skip_filter_validation = not _is_filter_validation_support()
|
||||
|
||||
def _is_native_sorting_support():
|
||||
native_sorting_attr_name = (
|
||||
"_%s__native_sorting_support" %
|
||||
@ -560,13 +566,13 @@ class NeutronDbPluginV2TestCase(testlib_api.WebTestCase):
|
||||
return self.deserialize(self.fmt, res)
|
||||
|
||||
def _list(self, resource, fmt=None, neutron_context=None,
|
||||
query_params=None):
|
||||
query_params=None, expected_code=webob.exc.HTTPOk.code):
|
||||
fmt = fmt or self.fmt
|
||||
req = self.new_list_request(resource, fmt, query_params)
|
||||
if neutron_context:
|
||||
req.environ['neutron.context'] = neutron_context
|
||||
res = req.get_response(self._api_for_resource(resource))
|
||||
self.assertEqual(webob.exc.HTTPOk.code, res.status_int)
|
||||
self.assertEqual(expected_code, res.status_int)
|
||||
return self.deserialize(fmt, res)
|
||||
|
||||
def _fail_second_call(self, patched_plugin, orig, *args, **kwargs):
|
||||
@ -595,13 +601,16 @@ class NeutronDbPluginV2TestCase(testlib_api.WebTestCase):
|
||||
self.assertEqual(items[1]['name'], 'test_1')
|
||||
|
||||
def _test_list_resources(self, resource, items, neutron_context=None,
|
||||
query_params=None):
|
||||
query_params=None,
|
||||
expected_code=webob.exc.HTTPOk.code):
|
||||
res = self._list('%ss' % resource,
|
||||
neutron_context=neutron_context,
|
||||
query_params=query_params)
|
||||
resource = resource.replace('-', '_')
|
||||
self.assertItemsEqual([i['id'] for i in res['%ss' % resource]],
|
||||
[i[resource]['id'] for i in items])
|
||||
query_params=query_params,
|
||||
expected_code=expected_code)
|
||||
if expected_code == webob.exc.HTTPOk.code:
|
||||
resource = resource.replace('-', '_')
|
||||
self.assertItemsEqual([i['id'] for i in res['%ss' % resource]],
|
||||
[i[resource]['id'] for i in items])
|
||||
|
||||
@contextlib.contextmanager
|
||||
def network(self, name='net1',
|
||||
@ -5018,6 +5027,8 @@ class TestSubnetsV2(NeutronDbPluginV2TestCase):
|
||||
query_params=query_params)
|
||||
|
||||
def test_list_subnets_filtering_by_unknown_filter(self):
|
||||
if self._skip_filter_validation:
|
||||
self.skipTest("Plugin does not support filter validation")
|
||||
with self.network() as network:
|
||||
with self.subnet(network=network,
|
||||
gateway_ip='10.0.0.1',
|
||||
@ -5028,11 +5039,13 @@ class TestSubnetsV2(NeutronDbPluginV2TestCase):
|
||||
subnets = (v1, v2)
|
||||
query_params = 'admin_state_up=True'
|
||||
self._test_list_resources('subnet', subnets,
|
||||
query_params=query_params)
|
||||
query_params=query_params,
|
||||
expected_code=webob.exc.HTTPClientError.code)
|
||||
# test with other value to check if we have the same results
|
||||
query_params = 'admin_state_up=False'
|
||||
self._test_list_resources('subnet', subnets,
|
||||
query_params=query_params)
|
||||
query_params=query_params,
|
||||
expected_code=webob.exc.HTTPClientError.code)
|
||||
|
||||
def test_list_subnets_with_parameter(self):
|
||||
with self.network() as network:
|
||||
|
@ -35,6 +35,7 @@ class MultiServiceCorePlugin(object):
|
||||
|
||||
|
||||
class CorePluginWithAgentNotifiers(object):
|
||||
supported_extension_aliases = []
|
||||
agent_notifiers = {'l3': 'l3_agent_notifier',
|
||||
'dhcp': 'dhcp_agent_notifier'}
|
||||
|
||||
|
@ -0,0 +1,38 @@
|
||||
---
|
||||
prelude: >
|
||||
Perform validation on filter parameters on listing resources.
|
||||
features:
|
||||
- |
|
||||
Starting from this release, neutron server will perform validation on
|
||||
filter parameters on list requests. Neutron will return a 400 response
|
||||
if the request contains invalid filter parameters.
|
||||
The list of valid parameters is documented in the neutron API reference.
|
||||
|
||||
Add an API extension ``filter-validation`` to indicate this new API
|
||||
behavior. This extension can be disabled by operators via a config option.
|
||||
upgrade:
|
||||
- |
|
||||
Prior to the upgrade, if a request contains an unknown or unsupported
|
||||
parameter, the server will silently ignore the invalid input.
|
||||
After the upgrade, the server will return a 400 Bad Request response
|
||||
instead.
|
||||
|
||||
API users might observe that requests that received a successful response
|
||||
now receive a failure response. If they encounter such experience,
|
||||
they are suggested to confirm if the API extension ``filter-validation``
|
||||
is present and validate filter parameters in their requests.
|
||||
|
||||
Operators can disable this feature if they want to maintain
|
||||
backward-compatibility. If they choose to do that, the API extension
|
||||
``filter-validation`` will not be present and the API behavior is
|
||||
unchanged.
|
||||
other:
|
||||
- |
|
||||
Each plugin can decide if it wants to support filter validation by
|
||||
setting ``__filter_validation_support`` to True or False. If this field is
|
||||
not set, the default value is False.
|
||||
Right now, the ML2 plugin and all the in-tree service plugins support
|
||||
filter validation. Out-of-tree plugins will have filter validation
|
||||
disabled by default but they can turn it on if they choose to.
|
||||
For filter validation to be supported, the core plugin and all the
|
||||
services plugins in a deployment must support it.
|
Loading…
Reference in New Issue
Block a user