Skip to content

Commit

Permalink
fix oas3 form data validation and type coercion
Browse files Browse the repository at this point in the history
  • Loading branch information
dtkav committed Aug 29, 2018
1 parent 09a5bc0 commit ce475a5
Show file tree
Hide file tree
Showing 2 changed files with 84 additions and 28 deletions.
70 changes: 52 additions & 18 deletions connexion/decorators/validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ def __str__(self):
def validate_type(param, value, parameter_type, parameter_name=None):
param_schema = param.get("schema", param)
param_type = param_schema.get('type')
parameter_name = parameter_name if parameter_name else param['name']
parameter_name = parameter_name if parameter_name else param.get('name')
if param_type == "array":
converted_params = []
for v in value:
Expand Down Expand Up @@ -91,10 +91,15 @@ def extend_with_nullable_support(validator_class):
def nullable_support(validator, properties, instance, schema):
null_properties = {}
for property_, subschema in six.iteritems(properties):
if isinstance(instance, collections.Iterable) and \
property_ in instance and \
instance[property_] is None and \
subschema.get('x-nullable') is True:
# check equal to True, because x-nullable is an extension
# whereas nullable value is validated by 3.0 spec
nullable = (subschema.get('x-nullable') is True or
subschema.get('nullable'))
has_property = (
isinstance(instance, collections.Iterable) and
property_ in instance
)
if has_property and instance[property_] is None and nullable:
# exclude from following validation
null_properties[property_] = instance.pop(property_)
for error in validate_properties(validator, properties, instance, schema):
Expand All @@ -109,6 +114,7 @@ def nullable_support(validator, properties, instance, schema):


class RequestBodyValidator(object):

def __init__(self, schema, consumes, api, is_null_value_valid=False, validator=None,
strict_validation=False):
"""
Expand All @@ -129,9 +135,32 @@ def __init__(self, schema, consumes, api, is_null_value_valid=False, validator=N
self.api = api
self.strict_validation = strict_validation

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

def coerce_types(self, param_schema, value, parameter_name=None):
param_type = param_schema["type"]
if is_nullable(param_schema) and is_null(value):
return None

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, 'form', parameter_name)
except TypeError:
return value

def __call__(self, function):
"""
Expand Down Expand Up @@ -171,18 +200,23 @@ def wrapper(request):
return error
elif self.consumes[0] in FORM_CONTENT_TYPES:
data = dict(request.form.items()) or (request.body if len(request.body) > 0 else {})
if data is None and len(request.body) > 0 and not self.is_null_value_valid:
# complain about no data?
pass
data.update(dict.fromkeys(request.files, '')) # validator expects string..
logger.debug('%s validating schema...', request.url)

if self.strict_validation:
formdata_errors = self.validate_requestbody_property_list(data)
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:
v = data[k]
data[k] = self.coerce_types(param_defn, v)

error = self.validate_schema(data, request.url)
if error and not self.has_default:
if error:
return error

response = function(request)
Expand Down Expand Up @@ -243,13 +277,13 @@ def __init__(self, parameters, api, strict_validation=False):
self.strict_validation = strict_validation

@staticmethod
def validate_parameter(parameter_type, value, param):
def validate_parameter(parameter_type, value, param, param_name=None):
if value is not None:
if is_nullable(param) and is_null(value):
return

try:
converted_value = validate_type(param, value, parameter_type)
converted_value = validate_type(param, value, parameter_type, param_name)
except TypeValidationError as e:
return str(e)

Expand Down Expand Up @@ -308,11 +342,11 @@ def validate_header_parameter(self, param, request):
val = request.headers.get(param['name'])
return self.validate_parameter('header', val, param)

def validate_formdata_parameter(self, param, request):
def validate_formdata_parameter(self, param_name, param, request):
if param.get('type') == 'file' or param.get('format') == 'binary':
val = request.files.get(param['name'])
val = request.files.get(param_name)
else:
val = request.form.get(param['name'])
val = request.form.get(param_name)

return self.validate_parameter('formdata', val, param)

Expand Down Expand Up @@ -352,7 +386,7 @@ def wrapper(request):
return self.api.get_response(response)

for param in self.parameters.get('formData', []):
error = self.validate_formdata_parameter(param, request)
error = self.validate_formdata_parameter(param["name"], param, request)
if error:
response = problem(400, 'Bad Request', error)
return self.api.get_response(response)
Expand Down
42 changes: 32 additions & 10 deletions connexion/operations/openapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -234,15 +234,37 @@ def body_definition(self):
return self.with_definitions(res)
return {}

def _get_body_argument(self, body, arguments, has_kwargs):
body_schema = self.body_schema
default_body = body_schema.get('default')
def _get_body_argument(self, body, arguments, has_kwargs, sanitize):

x_body_name = self.body_schema.get('x-body-name', 'body')
if is_nullable(self.body_schema) and is_null(body):
return {x_body_name: None}

default_body = self.body_schema.get('default', {})
body_props = {sanitize(k): {"schema": v} for k, v
in self.body_schema.get("properties", {}).items()}

body = body or default_body
if body_schema:
x_body_name = body_schema.get('x-body-name', 'body')
logger.debug('x-body-name is %s' % x_body_name)

if self.body_schema.get("type") != "object":
if x_body_name in arguments or has_kwargs:
return {x_body_name: body}
return {}

body_arg = deepcopy(default_body)
body_arg.update(body or {})

res = {}
if body_props:
for key, value in body_arg.items():
key = sanitize(key)
try:
prop_defn = body_props[key]
res[key] = self._get_val_from_param(value, prop_defn)
except KeyError: # pragma: no cover
logger.error("Body property '{}' not defined in body schema".format(key))
if x_body_name in arguments or has_kwargs:
return {x_body_name: res}
return {}

def get_arguments(self, path_params, query_params, body, files, arguments,
Expand All @@ -253,7 +275,7 @@ def get_arguments(self, path_params, query_params, body, files, arguments,
ret = {}
ret.update(self._get_path_arguments(path_params, sanitize))
ret.update(self._get_query_arguments(query_params, arguments, has_kwargs, sanitize))
ret.update(self._get_body_argument(body, arguments, has_kwargs))
ret.update(self._get_body_argument(body, arguments, has_kwargs, sanitize))
ret.update(self._get_file_arguments(files, arguments, has_kwargs))
return ret

Expand Down Expand Up @@ -284,11 +306,11 @@ def _get_query_arguments(self, query, arguments, has_kwargs, sanitize):
return res

def _get_val_from_param(self, value, query_defn):
if is_nullable(query_defn) and is_null(value):
return None

query_schema = query_defn["schema"]

if is_nullable(query_schema) and is_null(value):
return None

if query_schema["type"] == "array":
return [make_type(part, query_schema["items"]["type"]) for part in value]
else:
Expand Down

0 comments on commit ce475a5

Please sign in to comment.