Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Parse dont validate #1140

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion connexion/decorators/parameter.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ def wrapper(request):
if all_json(consumes):
request_body = request.json
elif consumes[0] in FORM_CONTENT_TYPES:
request_body = {sanitize(k): v for k, v in request.form.items()}
request_body = {k: v for k, v in request.form.items()}
else:
request_body = request.body

Expand Down
83 changes: 39 additions & 44 deletions connexion/decorators/validation.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import collections
import copy
import functools
import logging
Expand Down Expand Up @@ -71,15 +70,13 @@ def make_type(value, type_literal):
if param_schema.get('properties'):
def cast_leaves(d, schema):
if type(d) is not dict:
try:
return make_type(d, schema['type'])
except (ValueError, TypeError):
return d
return coerce_type(schema, d, parameter_type)
for k, v in d.items():
if k in schema['properties']:
d[k] = cast_leaves(v, schema['properties'][k])
elif schema.get('additionalProperites'):
Copy link
Contributor

@cognifloyd cognifloyd Jan 29, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how does this handle an explicit additionalProperties: true?
It looks like it calls cast_leaves -> coerce_type so param ends up as a True bool, so param.get("schema", param) (line 54) should fail.
If additionalProperties is not defined or None (ie the default value of True applies), then this skips handling it which is different than an explict true.

Suggested change
elif schema.get('additionalProperites'):
elif schema.get('additionalProperites') and not isinstance(schema.get('additionalProperties'), bool):

But that doesn't handle cases where additionalProperties are just allowed, and no schema for them is defined.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also spelling - 'additionalProperites' --> 'additionalProperties'

d[k] = cast_leaves(v, schema['additionalProperties'])
return d

return cast_leaves(value, param_schema)
return value
else:
Expand Down Expand Up @@ -157,7 +154,7 @@ def wrapper(request):
if data is not None or not self.has_default:
self.validate_schema(data, request.url)
elif self.consumes[0] in FORM_CONTENT_TYPES:
data = dict(request.form.items()) or (request.body if len(request.body) > 0 else {})
data = dict(request.form.items())
data.update(dict.fromkeys(request.files, '')) # validator expects string..
logger.debug('%s validating schema...', request.url)

Expand All @@ -167,17 +164,11 @@ def wrapper(request):
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:
raise BadRequestProblem(detail=errs)
try:
data = coerce_type(self.schema, data, 'requestBody')
request.form = data
except TypeValidationError as e:
raise BadRequestProblem(detail=[str(e)])

self.validate_schema(data, request.url)

Expand Down Expand Up @@ -240,8 +231,9 @@ def __init__(self, parameters, api, strict_validation=False):
:param api: api that the validator is attached to
:param strict_validation: Flag indicating if parameters not in spec are allowed
"""
self.parameters = collections.defaultdict(list)
self.parameters = {}
for p in parameters:
self.parameters.setdefault(p['in'], [])
self.parameters[p['in']].append(p)

self.api = api
Expand All @@ -251,12 +243,12 @@ def __init__(self, parameters, api, strict_validation=False):
def validate_parameter(parameter_type, value, param, param_name=None):
if value is not None:
if is_nullable(param) and is_null(value):
return
return None

try:
converted_value = coerce_type(param, value, parameter_type, param_name)
except TypeValidationError as e:
return str(e)
raise BadRequestProblem(detail=str(e))

param = copy.deepcopy(param)
param = param.get('schema', param)
Expand Down Expand Up @@ -290,10 +282,12 @@ def validate_parameter(parameter_type, value, param, param_name=None):
param=param
)
logger.info(debug_msg.format(**fmt_params))
return str(exception)
raise BadRequestProblem(detail=str(exception))

return converted_value

elif param.get('required'):
return "Missing {parameter_type} parameter '{param[name]}'".format(**locals())
raise BadRequestProblem(detail="Missing {parameter_type} parameter '{param[name]}'".format(**locals()))

def validate_query_parameter_list(self, request):
request_params = request.query.keys()
Expand All @@ -305,7 +299,7 @@ def validate_formdata_parameter_list(self, request):
try:
spec_params = [x['name'] for x in self.parameters['formData']]
except KeyError:
# OAS 3
# OAS 3, rely on additionalProperties validation
return set()
return validate_parameter_list(request_params, spec_params)

Expand All @@ -317,27 +311,38 @@ def validate_query_parameter(self, param, request):
:rtype: str
"""
val = request.query.get(param['name'])
return self.validate_parameter('query', val, param)
ret = self.validate_parameter('query', val, param)
if ret is not None:
request.query[param['name']] = ret

def validate_path_parameter(self, param, request):
val = request.path_params.get(param['name'].replace('-', '_'))
return self.validate_parameter('path', val, param)
ret = self.validate_parameter('path', val, param)
if ret is not None:
request.path_params[param['name']] = ret

def validate_header_parameter(self, param, request):
val = request.headers.get(param['name'])
return self.validate_parameter('header', val, param)
ret = self.validate_parameter('header', val, param)
if ret is not None:
request.headers[param['name']] = ret

def validate_cookie_parameter(self, param, request):
val = request.cookies.get(param['name'])
return self.validate_parameter('cookie', val, param)
ret = self.validate_parameter('cookie', val, param)
if ret is not None:
request.cookies[param['name']] = ret

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)
self.validate_parameter('formdata', val, param)
else:
val = request.form.get(param_name)
ret = self.validate_parameter('formdata', val, param)
if ret is not None:
request.form[param_name] = ret

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

def __call__(self, function):
"""
Expand All @@ -357,29 +362,19 @@ def wrapper(request):
raise ExtraParameterProblem(formdata_errors, query_errors)

for param in self.parameters.get('query', []):
error = self.validate_query_parameter(param, request)
if error:
raise BadRequestProblem(detail=error)
self.validate_query_parameter(param, request)

for param in self.parameters.get('path', []):
error = self.validate_path_parameter(param, request)
if error:
raise BadRequestProblem(detail=error)
self.validate_path_parameter(param, request)

for param in self.parameters.get('header', []):
error = self.validate_header_parameter(param, request)
if error:
raise BadRequestProblem(detail=error)
self.validate_header_parameter(param, request)

for param in self.parameters.get('cookie', []):
error = self.validate_cookie_parameter(param, request)
if error:
raise BadRequestProblem(detail=error)
self.validate_cookie_parameter(param, request)

for param in self.parameters.get('formData', []):
error = self.validate_formdata_parameter(param["name"], param, request)
if error:
raise BadRequestProblem(detail=error)
self.validate_formdata_parameter(param["name"], param, request)

return function(request)

Expand Down
96 changes: 24 additions & 72 deletions connexion/operations/openapi.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import logging
from copy import deepcopy
from copy import copy, deepcopy

from connexion.operations.abstract import AbstractOperation

Expand Down Expand Up @@ -245,81 +245,49 @@ def body_definition(self):

def _get_body_argument(self, body, arguments, has_kwargs, sanitize):
x_body_name = sanitize(self.body_schema.get('x-body-name', 'body'))
if x_body_name not in arguments and not has_kwargs:
# handler function won't accept the body
return {}

if is_nullable(self.body_schema) and is_null(body):
return {x_body_name: None}

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

# by OpenAPI specification `additionalProperties` defaults to `true`
# see: https://github.com/OAI/OpenAPI-Specification/blame/3.0.2/versions/3.0.2.md#L2305
additional_props = self.body_schema.get("additionalProperties", True)

if body is None:
body = deepcopy(default_body)

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 or additional_props:
res = self._get_typed_body_values(body_arg, body_props, additional_props)

if x_body_name in arguments or has_kwargs:
return {x_body_name: res}
return {}

def _get_typed_body_values(self, body_arg, body_props, additional_props):
"""
Return a copy of the provided body_arg dictionary
whose values will have the appropriate types
as defined in the provided schemas.

:type body_arg: type dict
:type body_props: dict
:type additional_props: dict|bool
:rtype: dict
"""
additional_props_defn = {"schema": additional_props} if isinstance(additional_props, dict) else None
res = {}
if self.body_schema.get("type") == "object":
default_body = self._get_default_obj(self.body_schema)
body = deep_merge(default_body, (body or {}))
else:
if body is None:
body = deepcopy(self.body_schema.get('default', {}))

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

return res
def _get_default_obj(self, schema):
try:
return deepcopy(schema["default"])
except KeyError:
_properties = schema.get("properties", {})
return self._build_default_obj_recursive(_properties, {})

def _build_default_obj(self, _properties, res={}):
def _build_default_obj_recursive(self, _properties, res):
""" takes disparate and nested default keys, and builds up a default object
"""
for key, prop in _properties.items():
if 'default' in prop and key not in res:
res[key] = prop['default']
res[key] = copy(prop['default'])
elif prop.get('type') == 'object' and 'properties' in prop:
res.setdefault(key, {})
res[key] = self._build_default_obj(prop['properties'], res[key])
res[key] = self._build_default_obj_recursive(prop['properties'], res[key])
return res

def _get_query_defaults(self, query_defns):
defaults = {}
for k, v in query_defns.items():
try:
if v["schema"]["type"] == "object":
defaults[k] = self._build_default_obj(v["schema"]["properties"])
defaults[k] = self._get_default_obj(v["schema"])
else:
defaults[k] = v["schema"]["default"]
except KeyError:
Expand All @@ -331,9 +299,7 @@ def _get_query_arguments(self, query, arguments, has_kwargs, sanitize):
for p in self.parameters
if p["in"] == "query"}
default_query_params = self._get_query_defaults(query_defns)

query_arguments = deepcopy(default_query_params)
query_arguments = deep_merge(query_arguments, query)
query_arguments = deep_merge(default_query_params, query)
return self._query_args_helper(query_defns, query_arguments,
arguments, has_kwargs, sanitize)

Expand All @@ -343,18 +309,4 @@ def _get_val_from_param(self, value, query_defn):
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]
elif query_schema["type"] == "object" and 'properties' in query_schema:
return_dict = {}
for prop_key in query_schema['properties'].keys():
prop_value = value.get(prop_key, None)
if prop_value is not None: # False is a valid value for boolean values
try:
return_dict[prop_key] = make_type(value[prop_key],
query_schema['properties'][prop_key]['type'])
except (KeyError, TypeError):
return value
return return_dict
else:
return make_type(value, query_schema["type"])
return value
21 changes: 9 additions & 12 deletions connexion/operations/swagger2.py
Original file line number Diff line number Diff line change
Expand Up @@ -240,13 +240,10 @@ def _get_body_argument(self, body, arguments, has_kwargs, sanitize):
body = deepcopy(body_parameters[0].get('schema', {}).get('default'))
body_name = sanitize(body_parameters[0].get('name'))

form_defns = {sanitize(p['name']): p
form_defns = {p['name']: p
for p in self.parameters
if p['in'] == 'formData'}

default_form_params = {k: v['default']
for k, v in form_defns.items()
if 'default' in v}

# Add body parameters
if body_name:
Expand All @@ -257,11 +254,16 @@ def _get_body_argument(self, body, arguments, has_kwargs, sanitize):
kwargs[body_name] = body

# Add formData parameters
default_form_params = {sanitize(k): v['default']
for k, v in form_defns.items()
if 'default' in v}
form_arguments = deepcopy(default_form_params)
if form_defns and body:
form_arguments.update(body)

for key, value in form_arguments.items():
if not has_kwargs and key not in arguments:
sanitized_key = sanitize(key)
if not has_kwargs and sanitized_key not in arguments:
logger.debug("FormData parameter '%s' not in function arguments", key)
else:
logger.debug("FormData parameter '%s' in function arguments", key)
Expand All @@ -270,16 +272,11 @@ def _get_body_argument(self, body, arguments, has_kwargs, sanitize):
except KeyError: # pragma: no cover
logger.error("Function argument '{}' not defined in specification".format(key))
else:
kwargs[key] = self._get_val_from_param(value, form_defn)
kwargs[sanitized_key] = self._get_val_from_param(value, form_defn)
return kwargs

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

query_schema = query_defn

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