Do not sanitize body keys in OpenAPI 3 (#1008)

* Remove the unused "query_sanitazion" fixture

* Test whether no sanitization is performed in the request body

* Do not perform sanitization on request body keys in OpenAPI v3

The deserialized JSON form of the request body
needs to be passed to the client applications
* without further modification *
so that they can work directly with objects
that have been received over the network.
The only names for which sanitization makes sense
are the ones which are used as Python identifiers.

Keys of the top-level JSON object within the request payload
are never used by Connexion as Python identifiers.

Also, no such sanitization of keys within request body
is performed in OpenAPI v2.

Closes issue #835.
This commit is contained in:
Peter Bašista
2019-12-03 05:01:49 +01:00
committed by Henning Jacobs
parent c4c7e677f0
commit 738f47ed50
8 changed files with 66 additions and 67 deletions

View File

@@ -244,12 +244,12 @@ class OpenAPIOperation(AbstractOperation):
return {} return {}
def _get_body_argument(self, body, arguments, has_kwargs, sanitize): def _get_body_argument(self, body, arguments, has_kwargs, sanitize):
x_body_name = self.body_schema.get('x-body-name', 'body') x_body_name = sanitize(self.body_schema.get('x-body-name', 'body'))
if is_nullable(self.body_schema) and is_null(body): if is_nullable(self.body_schema) and is_null(body):
return {x_body_name: None} return {x_body_name: None}
default_body = self.body_schema.get('default', {}) default_body = self.body_schema.get('default', {})
body_props = {sanitize(k): {"schema": v} for k, v body_props = {k: {"schema": v} for k, v
in self.body_schema.get("properties", {}).items()} in self.body_schema.get("properties", {}).items()}
# by OpenAPI specification `additionalProperties` defaults to `true` # by OpenAPI specification `additionalProperties` defaults to `true`
@@ -269,25 +269,27 @@ class OpenAPIOperation(AbstractOperation):
res = {} res = {}
if body_props or additional_props: if body_props or additional_props:
res = self._sanitize_body_argument(body_arg, body_props, additional_props, sanitize) res = self._get_typed_body_values(body_arg, body_props, additional_props)
if x_body_name in arguments or has_kwargs: if x_body_name in arguments or has_kwargs:
return {x_body_name: res} return {x_body_name: res}
return {} return {}
def _sanitize_body_argument(self, body_arg, body_props, additional_props, sanitize): 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_arg: type dict
:type body_props: dict :type body_props: dict
:type additional_props: dict|bool :type additional_props: dict|bool
:type sanitize: types.FunctionType
:rtype: dict :rtype: dict
""" """
additional_props_defn = {"schema": additional_props} if isinstance(additional_props, dict) else None additional_props_defn = {"schema": additional_props} if isinstance(additional_props, dict) else None
res = {} res = {}
for key, value in body_arg.items(): for key, value in body_arg.items():
key = sanitize(key)
try: try:
prop_defn = body_props[key] prop_defn = body_props[key]
res[key] = self._get_val_from_param(value, prop_defn) res[key] = self._get_val_from_param(value, prop_defn)

View File

@@ -403,6 +403,22 @@ def test_param_sanitization(simple_app):
assert resp.status_code == 200 assert resp.status_code == 200
assert json.loads(resp.data.decode('utf-8', 'replace')) == body assert json.loads(resp.data.decode('utf-8', 'replace')) == body
def test_no_sanitization_in_request_body(simple_app):
app_client = simple_app.app.test_client()
data = {
'name': 'John',
'$surname': 'Doe',
'1337': True,
'!#/bin/sh': False,
'(1/0)': 'division by zero',
's/$/EOL/': 'regular expression',
'@8am': 'time',
}
response = app_client.post('/v1.0/forward', json=data)
assert response.status_code == 200
assert response.json == data
def test_parameters_snake_case(snake_case_app): def test_parameters_snake_case(snake_case_app):
app_client = snake_case_app.app.test_client() app_client = snake_case_app.app.test_client()
headers = {'Content-type': 'application/json'} headers = {'Content-type': 'application/json'}

View File

@@ -179,11 +179,6 @@ def bad_operations_app(request):
resolver_error=501) resolver_error=501)
@pytest.fixture(scope="session", params=SPECS)
def query_sanitazion(request):
return build_app_from_fixture('query_sanitazion', request.param)
if sys.version_info < (3, 5, 3) and sys.version_info[0] == 3: if sys.version_info < (3, 5, 3) and sys.version_info[0] == 3:
@pytest.fixture @pytest.fixture
def aiohttp_client(test_client): def aiohttp_client(test_client):

View File

@@ -131,6 +131,11 @@ def schema(new_stack):
return new_stack return new_stack
def forward(body):
"""Return a response with the same payload as in the request body."""
return body
def schema_response_object(valid): def schema_response_object(valid):
if valid == "invalid_requirements": if valid == "invalid_requirements":
return {"docker_version": 1.0} return {"docker_version": 1.0}

View File

@@ -1,31 +0,0 @@
openapi: 3.0.0
info:
title: '{{title}}'
version: '1.0'
paths:
/greeting:
post:
summary: Generate greeting
description: Generates a greeting message.
operationId: fakeapi.hello.post_greeting
responses:
'200':
description: greeting response
content:
'application/json':
schema:
type: object
requestBody:
content:
application/x-www-form-urlencoded:
schema:
x-body-name: name
type: object
properties:
name:
description: Name of the person to greet.
type: string
required:
- name
servers:
- url: /v1.0

View File

@@ -1,22 +0,0 @@
swagger: "2.0"
info:
title: "{{title}}"
version: "1.0"
basePath: /v1.0
paths:
/greeting:
post:
summary: Generate greeting
description: Generates a greeting message.
operationId: fakeapi.hello.post_greeting
responses:
'200':
description: greeting response
schema:
type: object
parameters:
- name: name
in: formData
description: Name of the person to greet.
required: true
type: string

View File

@@ -1074,8 +1074,22 @@ paths:
trace: trace:
operationId: fakeapi.hello.trace_add_operation_on_http_methods_only operationId: fakeapi.hello.trace_add_operation_on_http_methods_only
responses: {} responses: {}
/forward:
post:
operationId: fakeapi.hello.forward
requestBody:
content:
application/json:
schema:
type: object
responses:
'200':
description: >
The response containing the same data as were present in request body.
content:
application/json:
schema:
type: object
servers: servers:
- url: http://localhost:{port}/{basePath} - url: http://localhost:{port}/{basePath}
@@ -1125,4 +1139,4 @@ components:
description: Name of the person to greet. description: Name of the person to greet.
required: false required: false
schema: schema:
type: string type: string

View File

@@ -964,6 +964,26 @@ paths:
items: items:
type: integer type: integer
/forward:
post:
operationId: fakeapi.hello.forward
consumes:
- application/json
produces:
- application/json
parameters:
- name: body
in: body
required: true
schema:
type: object
responses:
200:
description: >
The response containing the same data as were present in request body.
schema:
type: object
definitions: definitions:
new_stack: new_stack:
type: object type: object