From 695d5167504c131a03d83d370d49d5bb02abedad Mon Sep 17 00:00:00 2001 From: Igor Davydenko Date: Mon, 27 Jan 2020 14:46:05 +0200 Subject: [PATCH] fix: Ensure request body data relies only on immutable data structures. Fixes: #27 --- CHANGELOG.rst | 3 ++- rororo/openapi/mappings.py | 34 +++++++++++++++++----------------- rororo/openapi/validators.py | 4 ++-- tests/test_openapi.py | 16 ++++++++++++---- 4 files changed, 33 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index a8926bf..4838d69 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -4,8 +4,9 @@ 2.0.0b3 (In Development) ------------------------ -- feature: Provide human readable security & request parameter validation +- feature: Provide human readable security, request & response validation errors +- feature: Support free form objects in request body - feature: Allow to enable CORS / error middleware on setting up OpenAPI support for ``aiohttp.web`` application - feature: Provide ``BaseSettings`` and ``env_factory`` helpers to work with diff --git a/rororo/openapi/mappings.py b/rororo/openapi/mappings.py index 5d01b5b..3a3b390 100644 --- a/rororo/openapi/mappings.py +++ b/rororo/openapi/mappings.py @@ -7,25 +7,9 @@ from ..annotations import T -def convert_dict_to_mapping_proxy(data: Any) -> Any: - """Convert all dicts to mapping proxy types.""" - if isinstance(data, list): - return [convert_dict_to_mapping_proxy(item) for item in data] - - if isinstance(data, Mapping): - return types.MappingProxyType( - { - key: convert_dict_to_mapping_proxy(value) - for key, value in data.items() - } - ) - - return data - - def enforce_dicts(data: Any) -> Any: """Deep convert openapi-core Models to dicts.""" - if isinstance(data, list): + if isinstance(data, (list, tuple)): return [enforce_dicts(item) for item in data] if isinstance(data, Mapping): @@ -37,6 +21,22 @@ def enforce_dicts(data: Any) -> Any: return data +def enforce_immutable_data(data: Any) -> Any: + """Deep convert all dicts to mapping proxies & lists to tuples. + + This ensure that all request body data is immutable. + """ + if isinstance(data, list): + return tuple(enforce_immutable_data(item) for item in data) + + if isinstance(data, Mapping): + return types.MappingProxyType( + {key: enforce_immutable_data(value) for key, value in data.items()} + ) + + return data + + def merge_data(original_data: T, extra_data: Any) -> T: if isinstance(original_data, dict) and isinstance(extra_data, Mapping): for key, value in extra_data.items(): diff --git a/rororo/openapi/validators.py b/rororo/openapi/validators.py index 1cac4c3..5f1262c 100644 --- a/rororo/openapi/validators.py +++ b/rororo/openapi/validators.py @@ -14,7 +14,7 @@ to_openapi_parameters, ) from .exceptions import ValidationError -from .mappings import convert_dict_to_mapping_proxy, enforce_dicts, merge_data +from .mappings import enforce_dicts, enforce_immutable_data, merge_data CUSTOM_FORMATTERS = {"email": str} @@ -42,7 +42,7 @@ def _get_body( raw_body = operation.request_body.get_value(request) return ( - convert_dict_to_mapping_proxy( + enforce_immutable_data( merge_data(body_with_dicts, media_type.cast(raw_body)) ), errors, diff --git a/tests/test_openapi.py b/tests/test_openapi.py index 45b0bb0..78bceba 100644 --- a/tests/test_openapi.py +++ b/tests/test_openapi.py @@ -100,8 +100,14 @@ async def retrieve_nested_object_from_request_body( return web.json_response( {**enforce_dicts(context.data), "uid": str(context.data["uid"])}, headers={ - "X-Content-Data-Type": str(type(context.data)), - "X-UID-Data-Type": str(type(context.data["uid"])), + "X-Data-Type": str(type(context.data)), + "X-Data-Data-Data-Items-Type": str( + type(context.data["data"]["data_items"]) + ), + "X-Data-Data-Str-Items-Type": str( + type(context.data["data"]["str_items"]) + ), + "X-Data-UID-Type": str(type(context.data["uid"])), }, ) @@ -311,8 +317,10 @@ async def test_request_body_nested_obejct(aiohttp_client, schema_path): client = await aiohttp_client(app) response = await client.post("/api/nested-object", json=TEST_NESTED_OBJECT) assert response.status == 200 - assert response.headers["X-Content-Data-Type"] == "" - assert response.headers["X-UID-Data-Type"] == "" + assert response.headers["X-Data-Type"] == "" + assert response.headers["X-Data-Data-Data-Items-Type"] == "" + assert response.headers["X-Data-Data-Str-Items-Type"] == "" + assert response.headers["X-Data-UID-Type"] == "" assert await response.json() == TEST_NESTED_OBJECT