Merge "api: Add validation middleware"

This commit is contained in:
Zuul 2024-05-10 06:09:44 +00:00 committed by Gerrit Code Review
commit 1e0ef4aa00
7 changed files with 1150 additions and 5 deletions

View File

@ -27,15 +27,53 @@ from manila.api.openstack import wsgi
from manila.i18n import _
openstack_api_opts = [
cfg.StrOpt('project_id_regex',
default=r"[0-9a-f\-]+",
help=r'The validation regex for project_ids used in urls. '
r'This defaults to [0-9a-f\\-]+ if not set, '
r'which matches normal uuids created by keystone.'),
cfg.StrOpt(
'project_id_regex',
default=r'[0-9a-f\-]+',
help=(
r'The validation regex for project_ids used in URLs. '
r'This defaults to [0-9a-f\\-]+ if not set, '
r'which matches normal uuids created by keystone.'
),
),
]
validation_opts = [
cfg.StrOpt(
'response_validation',
choices=(
(
'error',
'Raise a HTTP 500 (Server Error) for responses that fail '
'schema validation',
),
(
'warn',
'Log a warning for responses that fail schema validation',
),
(
'ignore',
'Ignore schema validation failures',
),
),
default='warn',
help="""\
Configure validation of API responses.
``warn`` is the current recommendation for production environments. If you find
it necessary to enable the ``ignore`` option, please report the issues you are
seeing to the Manila team so we can improve our schemas.
``error`` should not be used in a production environment. This is because
schema validation happens *after* the response body has been generated, meaning
any side effects will still happen and the call may be non-idempotent despite
the user receiving a HTTP 500 error.
""",
),
]
CONF = cfg.CONF
CONF.register_opts(openstack_api_opts)
CONF.register_opts(validation_opts, group='api')
LOG = log.getLogger(__name__)

View File

@ -0,0 +1,230 @@
# 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.
"""API request/response validating middleware."""
import functools
import typing as ty
from oslo_serialization import jsonutils
import webob
from manila.api.openstack import api_version_request as api_version
from manila.api.openstack import wsgi
from manila.api.validation import validators
from manila import exception
from manila.i18n import _
def validated(cls):
cls._validated = True
return cls
def _schema_validator(
schema: ty.Dict[str, ty.Any],
target: ty.Dict[str, ty.Any],
min_version: ty.Optional[str],
max_version: ty.Optional[str],
args: ty.Any,
kwargs: ty.Any,
is_body: bool = True,
):
"""A helper method to execute JSON Schema Validation.
This method checks the request version whether matches the specified
``max_version`` and ``min_version``. If the version range matches the
request, we validate ``schema`` against ``target``. A failure will result
in ``ValidationError`` being raised.
:param schema: The JSON Schema schema used to validate the target.
:param target: The target to be validated by the schema.
:param min_version: A string indicating the minimum API version ``schema``
applies against.
:param max_version: A string indicating the maximum API version ``schema``
applies against.
:param args: Positional arguments which passed into original method.
:param kwargs: Keyword arguments which passed into original method.
:param is_body: Whether ``target`` is a HTTP request body or not.
:returns: None.
:raises: ``ValidationError`` if validation fails.
"""
min_ver = api_version.APIVersionRequest(min_version)
max_ver = api_version.APIVersionRequest(max_version)
# NOTE: The request object is always the second argument. However, numerous
# unittests pass in the request object via kwargs instead so we handle that
# as well.
# TODO(stephenfin): Fix unit tests so we don't have to to do this
if 'req' in kwargs:
ver = kwargs['req'].api_version_request
else:
ver = args[1].api_version_request
if ver.matches(min_ver, max_ver):
# Only validate against the schema if it lies within
# the version range specified. Note that if both min
# and max are not specified the validator will always
# be run.
schema_validator = validators._SchemaValidator(schema, is_body=is_body)
schema_validator.validate(target)
def request_body_schema(
schema: ty.Dict[str, ty.Any],
min_version: ty.Optional[str] = None,
max_version: ty.Optional[str] = None,
):
"""Register a schema to validate request body.
``schema`` will be used for validating the request body just before the API
method is executed.
:param schema: The JSON Schema schema used to validate the target.
:param min_version: A string indicating the minimum API version ``schema``
applies against.
:param max_version: A string indicating the maximum API version ``schema``
applies against.
"""
def add_validator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
_schema_validator(
schema,
kwargs['body'],
min_version,
max_version,
args,
kwargs,
is_body=True,
)
return func(*args, **kwargs)
wrapper._request_body_schema = schema
return wrapper
return add_validator
def request_query_schema(
schema: ty.Dict[str, ty.Any],
min_version: ty.Optional[str] = None,
max_version: ty.Optional[str] = None,
):
"""Register a schema to validate request query string parameters.
``schema`` will be used for validating request query strings just before
the API method is executed.
:param schema: The JSON Schema schema used to validate the target.
:param min_version: A string indicating the minimum API version ``schema``
applies against.
:param max_version: A string indicating the maximum API version ``schema``
applies against.
"""
def add_validator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
# NOTE: The request object is always the second argument. However,
# numerous unittests pass in the request object via kwargs instead
# so we handle that as well.
# TODO(stephenfin): Fix unit tests so we don't have to to do this
if 'req' in kwargs:
req = kwargs['req']
else:
req = args[1]
# NOTE: The webob package throws UnicodeError when param cannot be
# decoded. Catch this and raise HTTP 400.
try:
query = req.GET.dict_of_lists()
except UnicodeDecodeError:
msg = _('Query string is not UTF-8 encoded')
raise exception.ValidationError(msg)
_schema_validator(
schema,
query,
min_version,
max_version,
args,
kwargs,
is_body=True,
)
return func(*args, **kwargs)
wrapper._request_query_schema = schema
return wrapper
return add_validator
def response_body_schema(
schema: ty.Dict[str, ty.Any],
min_version: ty.Optional[str] = None,
max_version: ty.Optional[str] = None,
):
"""Register a schema to validate response body.
``schema`` will be used for validating the response body just after the API
method is executed.
:param schema: The JSON Schema schema used to validate the target.
:param min_version: A string indicating the minimum API version ``schema``
applies against.
:param max_version: A string indicating the maximum API version ``schema``
applies against.
"""
def add_validator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
response = func(*args, **kwargs)
# NOTE(stephenfin): If our response is an object, we need to
# serializer and deserialize to convert e.g. date-time to strings
if isinstance(response, wsgi.ResponseObject):
serializer = wsgi.JSONDictSerializer()
_body = serializer.serialize(response.obj)
# TODO(stephenfin): We should replace all instances of this with
# wsgi.ResponseObject
elif isinstance(response, webob.Response):
_body = response.body
else:
serializer = wsgi.JSONDictSerializer()
_body = serializer.serialize(response)
if _body == b'':
body = None
else:
body = jsonutils.loads(_body)
_schema_validator(
schema,
body,
min_version,
max_version,
args,
kwargs,
is_body=True,
)
return response
wrapper._response_body_schema = schema
return wrapper
return add_validator

View File

@ -0,0 +1,45 @@
# 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.
"""Common parameter types for validating API requests/responses."""
boolean = {
'type': ['boolean', 'string'],
'enum': [
True,
'True',
'TRUE',
'true',
'1',
'ON',
'On',
'on',
'YES',
'Yes',
'yes',
'y',
't',
False,
'False',
'FALSE',
'false',
'0',
'OFF',
'Off',
'off',
'NO',
'No',
'no',
'n',
'f',
],
}

View File

@ -0,0 +1,277 @@
# 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.
"""Internal implementation of request/response validating middleware."""
import re
import jsonschema
from jsonschema import exceptions as jsonschema_exc
from oslo_utils import timeutils
from oslo_utils import uuidutils
import webob.exc
from manila import exception
from manila.i18n import _
from manila import utils
def _soft_validate_additional_properties(
validator,
additional_properties_value,
param_value,
schema,
):
"""Validator function.
If there are not any properties on the param_value that are not specified
in the schema, this will return without any effect. If there are any such
extra properties, they will be handled as follows:
- if the validator passed to the method is not of type "object", this
method will return without any effect.
- if the 'additional_properties_value' parameter is True, this method will
return without any effect.
- if the schema has an additionalProperties value of True, the extra
properties on the param_value will not be touched.
- if the schema has an additionalProperties value of False and there
aren't patternProperties specified, the extra properties will be stripped
from the param_value.
- if the schema has an additionalProperties value of False and there
are patternProperties specified, the extra properties will not be
touched and raise validation error if pattern doesn't match.
"""
if not (
validator.is_type(param_value, "object") or additional_properties_value
):
return
properties = schema.get('properties', {})
patterns = '|'.join(schema.get('patternProperties', {}))
extra_properties = set()
for prop in param_value:
if prop not in properties:
if patterns:
if not re.search(patterns, prop):
extra_properties.add(prop)
else:
extra_properties.add(prop)
if not extra_properties:
return
if patterns:
error = 'Additional properties are not allowed (%s %s unexpected)'
if len(extra_properties) == 1:
verb = 'was'
else:
verb = 'were'
yield jsonschema_exc.ValidationError(
error
% (', '.join(repr(extra) for extra in extra_properties), verb)
)
else:
for prop in extra_properties:
del param_value[prop]
def _validate_string_length(
value,
entity_name,
mandatory=False,
min_length=0,
max_length=None,
remove_whitespaces=False,
):
"""Check the length of specified string.
:param value: the value of the string
:param entity_name: the name of the string
:mandatory: string is mandatory or not
:param min_length: the min_length of the string
:param max_length: the max_length of the string
:param remove_whitespaces: True if trimming whitespaces is needed else
False
"""
if not mandatory and not value:
return True
if mandatory and not value:
msg = _("The '%s' can not be None.") % entity_name
raise webob.exc.HTTPBadRequest(explanation=msg)
if remove_whitespaces:
value = value.strip()
utils.check_string_length(
value, entity_name, min_length=min_length, max_length=max_length
)
@jsonschema.FormatChecker.cls_checks('date-time')
def _validate_datetime_format(instance: object) -> bool:
# format checks constrain to the relevant primitive type
# https://github.com/OAI/OpenAPI-Specification/issues/3148
if not isinstance(instance, str):
return True
try:
timeutils.parse_isotime(instance)
except ValueError:
return False
else:
return True
@jsonschema.FormatChecker.cls_checks('uuid')
def _validate_uuid_format(instance: object) -> bool:
# format checks constrain to the relevant primitive type
# https://github.com/OAI/OpenAPI-Specification/issues/3148
if not isinstance(instance, str):
return True
return uuidutils.is_uuid_like(instance)
class FormatChecker(jsonschema.FormatChecker):
"""A FormatChecker can output the message from cause exception
We need understandable validation errors messages for users. When a
custom checker has an exception, the FormatChecker will output a
readable message provided by the checker.
"""
def check(self, param_value, format):
"""Check whether the param_value conforms to the given format.
:param param_value: the param_value to check
:type: any primitive type (str, number, bool)
:param str format: the format that param_value should conform to
:raises: :exc:`FormatError` if param_value does not conform to format
"""
if format not in self.checkers:
return
# For safety reasons custom checkers can be registered with
# allowed exception types. Anything else will fall into the
# default formatter.
func, raises = self.checkers[format]
result, cause = None, None
try:
result = func(param_value)
except raises as e:
cause = e
if not result:
msg = '%r is not a %r' % (param_value, format)
raise jsonschema_exc.FormatError(msg, cause=cause)
class _SchemaValidator(object):
"""A validator class
This class is changed from Draft202012Validator to validate minimum/maximum
value of a string number(e.g. '10').
In addition, FormatCheckers are added for checking data formats which are
common in the Manila API.
"""
validator = None
validator_org = jsonschema.Draft202012Validator
def __init__(
self, schema, relax_additional_properties=False, is_body=True
):
self.is_body = is_body
validators = {
'minimum': self._validate_minimum,
'maximum': self._validate_maximum,
}
if relax_additional_properties:
validators['additionalProperties'] = (
_soft_validate_additional_properties
)
validator_cls = jsonschema.validators.extend(
self.validator_org, validators
)
format_checker = FormatChecker()
self.validator = validator_cls(schema, format_checker=format_checker)
def validate(self, *args, **kwargs):
try:
self.validator.validate(*args, **kwargs)
except jsonschema.ValidationError as ex:
if len(ex.path) > 0:
if self.is_body:
# NOTE: For consistency across OpenStack services, this
# error message has been written in a similar format as
# WSME errors.
detail = _(
'Invalid input for field/attribute %(path)s. '
'Value: %(value)s. %(message)s'
) % {
'path': ex.path.pop(),
'value': ex.instance,
'message': ex.message,
}
else:
# NOTE: We use 'ex.path.popleft()' instead of
# 'ex.path.pop()'. This is due to the structure of query
# parameters which is a dict with key as name and value is
# list. As such, the first item in the 'ex.path' is the key
# and second item is the index of list in the value. We
# need the key as the parameter name in the error message
# so we pop the first value out of 'ex.path'.
detail = _(
'Invalid input for query parameters %(path)s. '
'Value: %(value)s. %(message)s'
) % {
'path': ex.path.popleft(),
'value': ex.instance,
'message': ex.message,
}
else:
detail = ex.message
raise exception.ValidationError(detail=detail)
except TypeError as ex:
# NOTE: If passing non string value to patternProperties parameter,
# TypeError happens. Here is for catching the TypeError.
detail = str(ex)
raise exception.ValidationError(detail=detail)
def _number_from_str(self, param_value):
try:
value = int(param_value)
except (ValueError, TypeError):
try:
value = float(param_value)
except (ValueError, TypeError):
return None
return value
def _validate_minimum(self, validator, minimum, param_value, schema):
param_value = self._number_from_str(param_value)
if param_value is None:
return
return self.validator_org.VALIDATORS['minimum'](
validator, minimum, param_value, schema
)
def _validate_maximum(self, validator, maximum, param_value, schema):
param_value = self._number_from_str(param_value)
if param_value is None:
return
return self.validator_org.VALIDATORS['maximum'](
validator, maximum, param_value, schema
)

View File

@ -199,6 +199,10 @@ class InvalidCapacity(Invalid):
message = _("Invalid capacity: %(name)s = %(value)s.")
class ValidationError(Invalid):
message = "%(detail)s"
class NotFound(ManilaException):
message = _("Resource could not be found.")
code = 404

View File

@ -0,0 +1,547 @@
# Copyright (C) 2017 NTT DATA
# 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 http import client as http
import re
from manila.api.openstack import api_version_request as api_version
from manila.api import validation
from manila.api.validation import parameter_types
from manila import exception
from manila import test
class FakeRequest(object):
api_version_request = api_version.APIVersionRequest("3.0")
environ = {}
class APIValidationTestCase(test.TestCase):
def setUp(self, schema=None):
super().setUp()
self.post = None
if schema is not None:
@validation.request_body_schema(schema=schema)
def post(req, body):
return 'Validation succeeded.'
self.post = post
def check_validation_error(self, method, body, expected_detail, req=None):
if not req:
req = FakeRequest()
try:
method(
body=body,
req=req,
)
except exception.ValidationError as ex:
self.assertEqual(http.BAD_REQUEST, ex.kwargs['code'])
if isinstance(expected_detail, list):
self.assertIn(
ex.kwargs['detail'],
expected_detail,
'Exception details did not match expected',
)
elif not re.match(expected_detail, ex.kwargs['detail']):
self.assertEqual(
expected_detail,
ex.kwargs['detail'],
'Exception details did not match expected',
)
except Exception as ex:
self.fail('An unexpected exception happens: %s' % ex)
else:
self.fail('Any exception did not happen.')
class RequiredDisableTestCase(APIValidationTestCase):
def setUp(self):
schema = {
'type': 'object',
'properties': {
'foo': {
'type': 'integer',
},
},
}
super().setUp(schema=schema)
def test_validate_required_disable(self):
self.assertEqual(
'Validation succeeded.',
self.post(body={'foo': 1}, req=FakeRequest()),
)
class RequiredEnableTestCase(APIValidationTestCase):
def setUp(self):
schema = {
'type': 'object',
'properties': {
'foo': {
'type': 'integer',
},
},
'required': ['foo'],
}
super().setUp(schema=schema)
def test_validate_required_enable(self):
self.assertEqual(
'Validation succeeded.',
self.post(body={'foo': 1}, req=FakeRequest()),
)
def test_validate_required_enable_fails(self):
detail = "'foo' is a required property"
self.check_validation_error(
self.post, body={'abc': 1}, expected_detail=detail
)
class AdditionalPropertiesEnableTestCase(APIValidationTestCase):
def setUp(self):
schema = {
'type': 'object',
'properties': {
'foo': {
'type': 'integer',
},
},
'required': ['foo'],
}
super().setUp(schema=schema)
def test_validate_additionalProperties_enable(self):
self.assertEqual(
'Validation succeeded.',
self.post(body={'foo': 1}, req=FakeRequest()),
)
self.assertEqual(
'Validation succeeded.',
self.post(body={'foo': 1, 'ext': 1}, req=FakeRequest()),
)
class AdditionalPropertiesDisableTestCase(APIValidationTestCase):
def setUp(self):
schema = {
'type': 'object',
'properties': {
'foo': {
'type': 'integer',
},
},
'required': ['foo'],
'additionalProperties': False,
}
super().setUp(schema=schema)
def test_validate_additionalProperties_disable(self):
self.assertEqual(
'Validation succeeded.',
self.post(body={'foo': 1}, req=FakeRequest()),
)
def test_validate_additionalProperties_disable_fails(self):
detail = "Additional properties are not allowed ('ext' was unexpected)"
self.check_validation_error(
self.post, body={'foo': 1, 'ext': 1}, expected_detail=detail
)
class PatternPropertiesTestCase(APIValidationTestCase):
def setUp(self):
schema = {
'patternProperties': {
'^[a-zA-Z0-9]{1,10}$': {'type': 'string'},
},
'additionalProperties': False,
}
super().setUp(schema=schema)
def test_validate_patternProperties(self):
self.assertEqual(
'Validation succeeded.',
self.post(body={'foo': 'bar'}, req=FakeRequest()),
)
def test_validate_patternProperties_fails(self):
details = [
"Additional properties are not allowed ('__' was unexpected)",
"'__' does not match any of the regexes: '^[a-zA-Z0-9]{1,10}$'",
]
self.check_validation_error(
self.post, body={'__': 'bar'}, expected_detail=details
)
details = [
"'' does not match any of the regexes: '^[a-zA-Z0-9]{1,10}$'",
"Additional properties are not allowed ('' was unexpected)",
]
self.check_validation_error(
self.post, body={'': 'bar'}, expected_detail=details
)
details = [
(
"'0123456789a' does not match any of the regexes: "
"'^[a-zA-Z0-9]{1,10}$'"
),
(
"Additional properties are not allowed ('0123456789a' was "
"unexpected)"
),
]
self.check_validation_error(
self.post, body={'0123456789a': 'bar'}, expected_detail=details
)
detail = "expected string or bytes-like object"
self.check_validation_error(
self.post, body={None: 'bar'}, expected_detail=detail
)
class StringTestCase(APIValidationTestCase):
def setUp(self):
schema = {
'type': 'object',
'properties': {
'foo': {
'type': 'string',
},
},
}
super().setUp(schema=schema)
def test_validate_string(self):
self.assertEqual(
'Validation succeeded.',
self.post(body={'foo': 'abc'}, req=FakeRequest()),
)
self.assertEqual(
'Validation succeeded.',
self.post(body={'foo': '0'}, req=FakeRequest()),
)
self.assertEqual(
'Validation succeeded.',
self.post(body={'foo': ''}, req=FakeRequest()),
)
def test_validate_string_fails(self):
detail = (
"Invalid input for field/attribute foo. Value: 1. "
"1 is not of type 'string'"
)
self.check_validation_error(
self.post, body={'foo': 1}, expected_detail=detail
)
detail = (
"Invalid input for field/attribute foo. Value: 1.5. "
"1.5 is not of type 'string'"
)
self.check_validation_error(
self.post, body={'foo': 1.5}, expected_detail=detail
)
detail = (
"Invalid input for field/attribute foo. Value: True. "
"True is not of type 'string'"
)
self.check_validation_error(
self.post, body={'foo': True}, expected_detail=detail
)
class StringLengthTestCase(APIValidationTestCase):
def setUp(self):
schema = {
'type': 'object',
'properties': {
'foo': {
'type': 'string',
'minLength': 1,
'maxLength': 10,
},
},
}
super().setUp(schema=schema)
def test_validate_string_length(self):
self.assertEqual(
'Validation succeeded.',
self.post(body={'foo': '0'}, req=FakeRequest()),
)
self.assertEqual(
'Validation succeeded.',
self.post(body={'foo': '0123456789'}, req=FakeRequest()),
)
def test_validate_string_length_fails(self):
# checks for jsonschema output from 3.2.x and 4.21.x
detail = (
"Invalid input for field/attribute foo. Value: . "
"'' (is too short|should be non-empty)"
)
self.check_validation_error(
self.post, body={'foo': ''}, expected_detail=detail
)
detail = (
"Invalid input for field/attribute foo. Value: 0123456789a. "
"'0123456789a' is too long"
)
self.check_validation_error(
self.post, body={'foo': '0123456789a'}, expected_detail=detail
)
class IntegerTestCase(APIValidationTestCase):
def setUp(self):
schema = {
'type': 'object',
'properties': {
'foo': {
'type': ['integer', 'string'],
'pattern': '^[0-9]+$',
},
},
}
super().setUp(schema=schema)
def test_validate_integer(self):
self.assertEqual(
'Validation succeeded.',
self.post(body={'foo': 1}, req=FakeRequest()),
)
self.assertEqual(
'Validation succeeded.',
self.post(body={'foo': '1'}, req=FakeRequest()),
)
self.assertEqual(
'Validation succeeded.',
self.post(body={'foo': '0123456789'}, req=FakeRequest()),
)
def test_validate_integer_fails(self):
detail = (
"Invalid input for field/attribute foo. Value: abc. "
"'abc' does not match '^[0-9]+$'"
)
self.check_validation_error(
self.post, body={'foo': 'abc'}, expected_detail=detail
)
detail = (
"Invalid input for field/attribute foo. Value: True. "
"True is not of type 'integer', 'string'"
)
self.check_validation_error(
self.post, body={'foo': True}, expected_detail=detail
)
detail = (
"Invalid input for field/attribute foo. Value: 0xffff. "
"'0xffff' does not match '^[0-9]+$'"
)
self.check_validation_error(
self.post, body={'foo': '0xffff'}, expected_detail=detail
)
detail = (
"Invalid input for field/attribute foo. Value: 1.01. "
"1.01 is not of type 'integer', 'string'"
)
self.check_validation_error(
self.post, body={'foo': 1.01}, expected_detail=detail
)
detail = (
"Invalid input for field/attribute foo. Value: 1.0. "
"'1.0' does not match '^[0-9]+$'"
)
self.check_validation_error(
self.post, body={'foo': '1.0'}, expected_detail=detail
)
class IntegerRangeTestCase(APIValidationTestCase):
def setUp(self):
schema = {
'type': 'object',
'properties': {
'foo': {
'type': ['integer', 'string'],
'pattern': '^[0-9]+$',
'minimum': 1,
'maximum': 10,
},
},
}
super().setUp(schema=schema)
def test_validate_integer_range(self):
self.assertEqual(
'Validation succeeded.',
self.post(body={'foo': 1}, req=FakeRequest()),
)
self.assertEqual(
'Validation succeeded.',
self.post(body={'foo': 10}, req=FakeRequest()),
)
self.assertEqual(
'Validation succeeded.',
self.post(body={'foo': '1'}, req=FakeRequest()),
)
def test_validate_integer_range_fails(self):
detail = (
"Invalid input for field/attribute foo. Value: 0. "
"0(.0)? is less than the minimum of 1"
)
self.check_validation_error(
self.post, body={'foo': 0}, expected_detail=detail
)
detail = (
"Invalid input for field/attribute foo. Value: 11. "
"11(.0)? is greater than the maximum of 10"
)
self.check_validation_error(
self.post, body={'foo': 11}, expected_detail=detail
)
detail = (
"Invalid input for field/attribute foo. Value: 0. "
"0(.0)? is less than the minimum of 1"
)
self.check_validation_error(
self.post, body={'foo': '0'}, expected_detail=detail
)
detail = (
"Invalid input for field/attribute foo. Value: 11. "
"11(.0)? is greater than the maximum of 10"
)
self.check_validation_error(
self.post, body={'foo': '11'}, expected_detail=detail
)
class BooleanTestCase(APIValidationTestCase):
def setUp(self):
schema = {
'type': 'object',
'properties': {
'foo': parameter_types.boolean,
},
}
super().setUp(schema=schema)
def test_validate_boolean(self):
self.assertEqual(
'Validation succeeded.',
self.post(body={'foo': True}, req=FakeRequest()),
)
self.assertEqual(
'Validation succeeded.',
self.post(body={'foo': False}, req=FakeRequest()),
)
self.assertEqual(
'Validation succeeded.',
self.post(body={'foo': 'True'}, req=FakeRequest()),
)
self.assertEqual(
'Validation succeeded.',
self.post(body={'foo': 'False'}, req=FakeRequest()),
)
self.assertEqual(
'Validation succeeded.',
self.post(body={'foo': '1'}, req=FakeRequest()),
)
self.assertEqual(
'Validation succeeded.',
self.post(body={'foo': '0'}, req=FakeRequest()),
)
def test_validate_boolean_fails(self):
enum_boolean = (
"[True, 'True', 'TRUE', 'true', '1', 'ON', 'On', "
"'on', 'YES', 'Yes', 'yes', 'y', 't', "
"False, 'False', 'FALSE', 'false', '0', 'OFF', 'Off', "
"'off', 'NO', 'No', 'no', 'n', 'f']"
)
detail = (
"Invalid input for field/attribute foo. Value: bar. "
"'bar' is not one of %s"
) % enum_boolean
self.check_validation_error(
self.post, body={'foo': 'bar'}, expected_detail=detail
)
detail = (
"Invalid input for field/attribute foo. Value: 2. "
"'2' is not one of %s"
) % enum_boolean
self.check_validation_error(
self.post, body={'foo': '2'}, expected_detail=detail
)
class DatetimeTestCase(APIValidationTestCase):
def setUp(self):
schema = {
'type': 'object',
'properties': {
'foo': {
'type': ['string', 'null'],
'format': 'date-time',
},
},
}
super().setUp(schema=schema)
def test_validate_datetime(self):
self.assertEqual(
'Validation succeeded.',
self.post(body={'foo': '2017-01-14T01:00:00Z'}, req=FakeRequest()),
)
self.assertEqual(
'Validation succeeded.',
self.post(body={'foo': None}, req=FakeRequest()),
)
def test_validate_datetime_fails(self):
detail = (
"Invalid input for field/attribute foo. Value: True. "
"True is not of type 'string', 'null'"
)
self.check_validation_error(
self.post, body={'foo': True}, expected_detail=detail
)
detail = (
"Invalid input for field/attribute foo. Value: 123. "
"'123' is not a 'date-time'"
)
self.check_validation_error(
self.post, body={'foo': '123'}, expected_detail=detail
)

View File

@ -19,6 +19,8 @@ import os
from oslo_policy import opts
from oslo_service import wsgi
# some of these are imported for their side-effects
from manila.api import openstack # noqa
from manila.common import config
CONF = config.CONF
@ -82,6 +84,8 @@ def set_defaults(conf):
_safe_set_of_opts(conf, 'unity_server_meta_pool', 'nas_server_pool')
conf.set_default('response_validation', 'error', group='api')
def _safe_set_of_opts(conf, *args, **kwargs):
try: