Skip to content

Commit

Permalink
redo validation by content type
Browse files Browse the repository at this point in the history
  • Loading branch information
dtkav committed Nov 10, 2018
1 parent fcba5af commit d8429d6
Show file tree
Hide file tree
Showing 11 changed files with 274 additions and 150 deletions.
8 changes: 3 additions & 5 deletions connexion/decorators/parameter.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,8 @@
import inflection
import six

from ..http_facts import FORM_CONTENT_TYPES
from ..lifecycle import ConnexionRequest # NOQA
from ..utils import all_json
from ..utils import is_form_mimetype, is_json_mimetype

try:
import builtins
Expand Down Expand Up @@ -73,7 +72,6 @@ def parameter_to_arg(operation, function, pythonic_params=False,
request context will be passed as that argument.
:type pass_context_arg_name: str|None
"""
consumes = operation.consumes

def sanitized(name):
return name and re.sub('^[^a-zA-Z_]+', '', re.sub('[^0-9a-zA-Z_]', '', name))
Expand All @@ -91,9 +89,9 @@ def wrapper(request):
logger.debug('Function Arguments: %s', arguments)
kwargs = {}

if all_json(consumes):
if is_json_mimetype(request.content_type):
request_body = request.json
elif consumes[0] in FORM_CONTENT_TYPES:
elif is_form_mimetype(request.content_type):
request_body = {sanitize(k): v for k, v in request.form.items()}
else:
request_body = request.body
Expand Down
165 changes: 36 additions & 129 deletions connexion/decorators/validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,68 +9,14 @@
from werkzeug import FileStorage

from ..exceptions import ExtraParameterProblem
from ..http_facts import FORM_CONTENT_TYPES
from ..json_schema import Draft4RequestValidator, Draft4ResponseValidator
from ..problem import problem
from ..utils import all_json, boolean, is_json_mimetype, is_null, is_nullable
from ..serialization.deserializers import DEFAULT_DESERIALIZERS
from ..types import TypeValidationError, coerce_type
from ..utils import is_null, is_nullable

logger = logging.getLogger('connexion.decorators.validation')

TYPE_MAP = {
'integer': int,
'number': float,
'boolean': boolean
}


class TypeValidationError(Exception):
def __init__(self, schema_type, parameter_type, parameter_name):
"""
Exception raise when type validation fails
:type schema_type: str
:type parameter_type: str
:type parameter_name: str
:return:
"""
self.schema_type = schema_type
self.parameter_type = parameter_type
self.parameter_name = parameter_name

def __str__(self):
msg = "Wrong type, expected '{schema_type}' for {parameter_type} parameter '{parameter_name}'"
return msg.format(**vars(self))


def coerce_type(param, value, parameter_type, parameter_name=None):

def make_type(value, type_literal):
type_func = TYPE_MAP.get(type_literal)
return type_func(value)

param_schema = param.get("schema", param)
if is_nullable(param_schema) and is_null(value):
return None

param_type = param_schema.get('type')
parameter_name = parameter_name if parameter_name else param.get('name')
if param_type == "array":
converted_params = []
for v in value:
try:
converted = make_type(v, param_schema["items"]["type"])
except (ValueError, TypeError):
converted = v
converted_params.append(converted)
return converted_params
else:
try:
return make_type(value, param_type)
except ValueError:
raise TypeValidationError(param_type, parameter_type, parameter_name)
except TypeError:
return value


def validate_parameter_list(request_params, spec_params):
request_params = set(request_params)
Expand Down Expand Up @@ -100,11 +46,25 @@ def __init__(self, schema, consumes, api, is_null_value_valid=False, validator=N
self.validator = validatorClass(schema, format_checker=draft4_format_checker)
self.api = api
self.strict_validation = strict_validation

def validate_formdata_parameter_list(self, request):
request_params = request.form.keys()
spec_params = self.schema.get('properties', {}).keys()
return validate_parameter_list(request_params, spec_params)
self._content_handlers = [
de(self.validator,
self.schema,
self.strict_validation,
self.is_null_value_valid) for de in DEFAULT_DESERIALIZERS
]

def register_content_handler(self, cv):
self._content_handlers += [cv]

def lookup_content_handler(self, request):
matches = [
v for v in self._content_handlers
if v.regex.match(request.content_type)
]
if len(matches) > 1:
logger.warning("Content could be handled by multiple validators")
if matches:
return matches[0]

def __call__(self, function):
"""
Expand All @@ -114,81 +74,28 @@ def __call__(self, function):

@functools.wraps(function)
def wrapper(request):
if all_json(self.consumes):
data = request.json

empty_body = not(request.body or request.form or request.files)
if data is None and not empty_body and not self.is_null_value_valid:
try:
ctype_is_json = is_json_mimetype(request.headers.get("Content-Type", ""))
except ValueError:
ctype_is_json = False

if ctype_is_json:
# Content-Type is json but actual body was not parsed
return problem(400,
"Bad Request",
"Request body is not valid JSON"
)
else:
# the body has contents that were not parsed as JSON
return problem(415,
"Unsupported Media Type",
"Invalid Content-type ({content_type}), expected JSON data".format(
content_type=request.headers.get("Content-Type", "")
))

logger.debug("%s validating schema...", request.url)
error = self.validate_schema(data, request.url)
if error and not self.has_default:
return error
elif self.consumes[0] in FORM_CONTENT_TYPES:
data = dict(request.form.items()) or (request.body if len(request.body) > 0 else {})
data.update(dict.fromkeys(request.files, '')) # validator expects string..
logger.debug('%s validating schema...', request.url)

if self.strict_validation:
formdata_errors = self.validate_formdata_parameter_list(request)
if formdata_errors:
raise ExtraParameterProblem(formdata_errors, [])

if data:
props = self.schema.get("properties", {})
errs = []
for k, param_defn in props.items():
if k in data:
try:
data[k] = coerce_type(param_defn, data[k], 'requestBody', k)
except TypeValidationError as e:
errs += [str(e)]
print(errs)
if errs:
return problem(400, 'Bad Request', errs)

error = self.validate_schema(data, request.url)
content_handler = self.lookup_content_handler(request)
exact_match = request.content_type in self.consumes
partial_match = content_handler and content_handler.name in self.consumes
if not (exact_match or partial_match):
msg = "Invalid Content-type ({content_type}), expected one of {consumes}"
msg = msg.format(content_type=request.content_type, consumes=self.consumes)
return problem(415, "Unsupported Media Type", msg)

if content_handler:
error = content_handler.validate(request)
if error:
return error

elif self.strict_validation:
logger.warning("No handler for ({content_type})".format(
content_type=request.content_type))

response = function(request)
return response

return wrapper

def validate_schema(self, data, url):
# type: (dict, AnyStr) -> Union[ConnexionResponse, None]
if self.is_null_value_valid and is_null(data):
return None

try:
self.validator.validate(data)
except ValidationError as exception:
logger.error("{url} validation error: {error}".format(url=url,
error=exception.message),
extra={'validator': 'body'})
return problem(400, 'Bad Request', str(exception.message))

return None


class ResponseBodyValidator(object):
def __init__(self, schema, validator=None):
Expand Down
2 changes: 1 addition & 1 deletion connexion/lifecycle.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

class ConnexionRequest(object):
def __init__(self,
url,
Expand All @@ -21,6 +20,7 @@ def __init__(self,
self.json_getter = json_getter
self.files = files
self.context = context if context is not None else {}
self.content_type = self.headers.get("Content-Type", "")

@property
def json(self):
Expand Down
Empty file.
123 changes: 123 additions & 0 deletions connexion/serialization/deserializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import logging
import re

from jsonschema import ValidationError

from ..exceptions import ExtraParameterProblem
from ..problem import problem
from ..types import coerce_type
from ..utils import is_null

logger = logging.getLogger('connexion.serialization.deserializers')


class ContentHandler(object):

def __init__(self, validator, schema, strict, is_null_value_valid):
self.schema = schema
self.strict_validation = strict
self.is_null_value_valid = is_null_value_valid
self.validator = validator
self.has_default = schema.get('default', False)

def validate_schema(self, data, url):
# type: (dict, AnyStr) -> Union[ConnexionResponse, None]
if self.is_null_value_valid and is_null(data):
return None

try:
self.validator.validate(data)
except ValidationError as exception:
logger.error("{url} validation error: {error}".format(url=url,
error=exception.message),
extra={'validator': 'body'})
return problem(400, 'Bad Request', str(exception.message))

return None

def _deser(self, request):
return request.body

def validate(self, request):
data = self._deser(request)
errors = self.validate_schema(data, request.url)
if errors and not self.has_default:
return errors


class StreamingContentHandler(ContentHandler):
name = "application/octet-stream"
regex = re.compile(r'^application\/octet-stream.*')

def validate(self, request):
# Don't validate, leave stream for user to read
pass


class JSONContentHandler(ContentHandler):
name = "application/json"
regex = re.compile(r'^application\/json.*|^.*\+json$')

def _deser(self, request):
data = request.json
empty_body = not(request.body or request.form or request.files)
if data is None and not empty_body and not self.is_null_value_valid:
# Content-Type is json but actual body was not parsed
return problem(400,
"Bad Request",
"Request body is not valid JSON"
)
return data


def validate_parameter_list(request_params, spec_params):
request_params = set(request_params)
spec_params = set(spec_params)

return request_params.difference(spec_params)


class FormDataContentHandler(ContentHandler):
name = "application/x-www-form-urlencoded"
regex = re.compile(
r'^application\/x-www-form-urlencoded.*'
)

def validate_formdata_parameter_list(self, request):
request_params = request.form.keys()
spec_params = self.schema.get('properties', {}).keys()
return validate_parameter_list(request_params, spec_params)

def _deser(self, request):
data = dict(request.form.items()) or \
(request.body if len(request.body) > 0 else {})
data.update(dict.fromkeys(request.files, '')) # validator expects string..
logger.debug('%s validating schema...', request.url)

if self.strict_validation:
formdata_errors = self.validate_formdata_parameter_list(request)
if formdata_errors:
raise ExtraParameterProblem(formdata_errors, [])

if data:
props = self.schema.get("properties", {})
for k, param_defn in props.items():
if k in data:
data[k] = coerce_type(param_defn, data[k], 'requestBody', k)

return data


class MultiPartFormDataContentHandler(FormDataContentHandler):
name = "multipart/form-data"
regex = re.compile(
r'^multipart\/form-data.*'
)


DEFAULT_DESERIALIZERS = [
StreamingContentHandler,
JSONContentHandler,
FormDataContentHandler,
MultiPartFormDataContentHandler
]
Loading

0 comments on commit d8429d6

Please sign in to comment.