From 6e2c84e6e1d28da18d8d6435f41915524e82fa7a Mon Sep 17 00:00:00 2001 From: Michael Brewer Date: Sat, 31 Jul 2021 17:14:17 -0700 Subject: [PATCH 1/4] feat(event-handler): Allow for a configured common prefix --- .../event_handler/api_gateway.py | 14 ++++++++++++- .../event_handler/test_api_gateway.py | 20 +++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/aws_lambda_powertools/event_handler/api_gateway.py b/aws_lambda_powertools/event_handler/api_gateway.py index 7bf364695da..d8a229f28f9 100644 --- a/aws_lambda_powertools/event_handler/api_gateway.py +++ b/aws_lambda_powertools/event_handler/api_gateway.py @@ -265,6 +265,7 @@ def __init__( cors: Optional[CORSConfig] = None, debug: Optional[bool] = None, serializer: Optional[Callable[[Dict], str]] = None, + prefix: Optional[str] = None, ): """ Parameters @@ -276,6 +277,10 @@ def __init__( debug: Optional[bool] Enables debug mode, by default False. Can be also be enabled by "POWERTOOLS_EVENT_HANDLER_DEBUG" environment variable + serializer : Callable, optional + function to serialize `obj` to a JSON formatted `str`, by default json.dumps + prefix: str, optional + optional prefix removed from the path before doing the routing """ self._proxy_type = proxy_type self._routes: List[Route] = [] @@ -285,6 +290,7 @@ def __init__( self._debug = resolve_truthy_env_var_choice( env=os.getenv(constants.EVENT_HANDLER_DEBUG_ENV, "false"), choice=debug ) + self._prefix = prefix # Allow for a custom serializer or a concise json serialization self._serializer = serializer or partial(json.dumps, separators=(",", ":"), cls=Encoder) @@ -521,7 +527,7 @@ def _to_proxy_event(self, event: Dict) -> BaseProxyEvent: def _resolve(self) -> ResponseBuilder: """Resolves the response or return the not found response""" method = self.current_event.http_method.upper() - path = self.current_event.path + path = self._remove_prefix(self.current_event.path) for route in self._routes: if method != route.method: continue @@ -533,6 +539,12 @@ def _resolve(self) -> ResponseBuilder: logger.debug(f"No match found for path {path} and method {method}") return self._not_found(method) + def _remove_prefix(self, path: str) -> str: + """Remove the configured prefix from the path""" + if self._prefix and path.startswith(self._prefix): + return path[len(self._prefix) :] + return path + def _not_found(self, method: str) -> ResponseBuilder: """Called when no matching route was found and includes support for the cors preflight response""" headers = {} diff --git a/tests/functional/event_handler/test_api_gateway.py b/tests/functional/event_handler/test_api_gateway.py index 1272125da8b..98f7d85b1cc 100644 --- a/tests/functional/event_handler/test_api_gateway.py +++ b/tests/functional/event_handler/test_api_gateway.py @@ -769,3 +769,23 @@ def get_color() -> Dict: body = response["body"] expected = '{"color": 1, "variations": ["dark", "light"]}' assert expected == body + + +@pytest.mark.parametrize( + "path", + [pytest.param("/pay/foo", id="prefix matches path"), pytest.param("/foo", id="prefix does not match path")], +) +def test_remove_prefix(path: str): + # GIVEN a configured prefix of `/pay` + # AND events paths `/pay/foo` or `/foo` + app = ApiGatewayResolver(prefix="/pay") + + @app.get("/foo") + def foo(): + ... + + # WHEN calling handler + response = app({"httpMethod": "GET", "path": path}, None) + + # THEN a route for `/foo` should be found + assert response["statusCode"] == 200 From 008afad8ad1ce174c09f0e68b2b711126c0d0b99 Mon Sep 17 00:00:00 2001 From: Michael Brewer Date: Wed, 18 Aug 2021 13:44:50 -0700 Subject: [PATCH 2/4] refactor: change to support multiple --- .../event_handler/api_gateway.py | 15 +++++++++------ .../functional/event_handler/test_api_gateway.py | 8 ++++++-- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/aws_lambda_powertools/event_handler/api_gateway.py b/aws_lambda_powertools/event_handler/api_gateway.py index 59adb00f571..f81d5aabd4c 100644 --- a/aws_lambda_powertools/event_handler/api_gateway.py +++ b/aws_lambda_powertools/event_handler/api_gateway.py @@ -265,7 +265,7 @@ def __init__( cors: Optional[CORSConfig] = None, debug: Optional[bool] = None, serializer: Optional[Callable[[Dict], str]] = None, - prefix: Optional[str] = None, + strip_prefixes: Optional[List[str]] = None, ): """ Parameters @@ -279,8 +279,8 @@ def __init__( environment variable serializer : Callable, optional function to serialize `obj` to a JSON formatted `str`, by default json.dumps - prefix: str, optional - optional prefix removed from the path before doing the routing + strip_prefixes: List[str], optional + optional list of prefixes to be removed from the path before doing the routing """ self._proxy_type = proxy_type self._routes: List[Route] = [] @@ -290,7 +290,7 @@ def __init__( self._debug = resolve_truthy_env_var_choice( env=os.getenv(constants.EVENT_HANDLER_DEBUG_ENV, "false"), choice=debug ) - self._prefix = prefix + self._strip_prefixes = strip_prefixes # Allow for a custom serializer or a concise json serialization self._serializer = serializer or partial(json.dumps, separators=(",", ":"), cls=Encoder) @@ -541,8 +541,11 @@ def _resolve(self) -> ResponseBuilder: def _remove_prefix(self, path: str) -> str: """Remove the configured prefix from the path""" - if self._prefix and path.startswith(self._prefix): - return path[len(self._prefix) :] + if self._strip_prefixes: + for prefix in self._strip_prefixes: + if path.startswith(prefix + "/"): + return path[len(prefix) :] + return path def _not_found(self, method: str) -> ResponseBuilder: diff --git a/tests/functional/event_handler/test_api_gateway.py b/tests/functional/event_handler/test_api_gateway.py index 98f7d85b1cc..bed47e24632 100644 --- a/tests/functional/event_handler/test_api_gateway.py +++ b/tests/functional/event_handler/test_api_gateway.py @@ -773,12 +773,16 @@ def get_color() -> Dict: @pytest.mark.parametrize( "path", - [pytest.param("/pay/foo", id="prefix matches path"), pytest.param("/foo", id="prefix does not match path")], + [ + pytest.param("/pay/foo", id="prefix matches path"), + pytest.param("/payment/foo", id="prefix matches path"), + pytest.param("/foo", id="prefix does not match path"), + ], ) def test_remove_prefix(path: str): # GIVEN a configured prefix of `/pay` # AND events paths `/pay/foo` or `/foo` - app = ApiGatewayResolver(prefix="/pay") + app = ApiGatewayResolver(strip_prefixes=["/pay", "/payment"]) @app.get("/foo") def foo(): From e492769040674bc16f5ab017ab60589d14779924 Mon Sep 17 00:00:00 2001 From: Michael Brewer Date: Wed, 18 Aug 2021 21:10:02 -0700 Subject: [PATCH 3/4] chore: better docs --- aws_lambda_powertools/event_handler/api_gateway.py | 5 +++-- tests/functional/event_handler/test_api_gateway.py | 14 +++++++++----- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/aws_lambda_powertools/event_handler/api_gateway.py b/aws_lambda_powertools/event_handler/api_gateway.py index f81d5aabd4c..a8a8629a51e 100644 --- a/aws_lambda_powertools/event_handler/api_gateway.py +++ b/aws_lambda_powertools/event_handler/api_gateway.py @@ -37,7 +37,7 @@ class ProxyEventType(Enum): ALBEvent = "ALBEvent" -class CORSConfig(object): +class CORSConfig: """CORS Config Examples @@ -280,7 +280,8 @@ def __init__( serializer : Callable, optional function to serialize `obj` to a JSON formatted `str`, by default json.dumps strip_prefixes: List[str], optional - optional list of prefixes to be removed from the path before doing the routing + optional list of prefixes to be removed from the request path before doing the routing. This is often used + with api gateways with multiple custom mappings. """ self._proxy_type = proxy_type self._routes: List[Route] = [] diff --git a/tests/functional/event_handler/test_api_gateway.py b/tests/functional/event_handler/test_api_gateway.py index bed47e24632..4db08dd0890 100644 --- a/tests/functional/event_handler/test_api_gateway.py +++ b/tests/functional/event_handler/test_api_gateway.py @@ -774,16 +774,20 @@ def get_color() -> Dict: @pytest.mark.parametrize( "path", [ - pytest.param("/pay/foo", id="prefix matches path"), - pytest.param("/payment/foo", id="prefix matches path"), - pytest.param("/foo", id="prefix does not match path"), + pytest.param("/pay/foo", id="path matched pay prefix"), + pytest.param("/payment/foo", id="path matched payment prefix"), + pytest.param("/foo", id="path does not start with any of the prefixes"), ], ) def test_remove_prefix(path: str): - # GIVEN a configured prefix of `/pay` - # AND events paths `/pay/foo` or `/foo` + # GIVEN events paths `/pay/foo`, `/payment/foo` or `/foo` + # AND a configured strip_prefixes of `/pay` and `/payment` app = ApiGatewayResolver(strip_prefixes=["/pay", "/payment"]) + @app.get("/pay/foo") + def pay_foo(): + raise ValueError("should not be matching") + @app.get("/foo") def foo(): ... From 1d6b49d490c00907674c9f4ffd4e7914147e96ad Mon Sep 17 00:00:00 2001 From: Michael Brewer Date: Thu, 19 Aug 2021 00:20:50 -0700 Subject: [PATCH 4/4] fix: add more input validation --- .../event_handler/api_gateway.py | 18 ++++++++++--- .../event_handler/test_api_gateway.py | 26 +++++++++++++++++++ 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/aws_lambda_powertools/event_handler/api_gateway.py b/aws_lambda_powertools/event_handler/api_gateway.py index a8a8629a51e..c1cdde63db9 100644 --- a/aws_lambda_powertools/event_handler/api_gateway.py +++ b/aws_lambda_powertools/event_handler/api_gateway.py @@ -542,13 +542,23 @@ def _resolve(self) -> ResponseBuilder: def _remove_prefix(self, path: str) -> str: """Remove the configured prefix from the path""" - if self._strip_prefixes: - for prefix in self._strip_prefixes: - if path.startswith(prefix + "/"): - return path[len(prefix) :] + if not isinstance(self._strip_prefixes, list): + return path + + for prefix in self._strip_prefixes: + if self._path_starts_with(path, prefix): + return path[len(prefix) :] return path + @staticmethod + def _path_starts_with(path: str, prefix: str): + """Returns true if the `path` starts with a prefix plus a `/`""" + if not isinstance(prefix, str) or len(prefix) == 0: + return False + + return path.startswith(prefix + "/") + def _not_found(self, method: str) -> ResponseBuilder: """Called when no matching route was found and includes support for the cors preflight response""" headers = {} diff --git a/tests/functional/event_handler/test_api_gateway.py b/tests/functional/event_handler/test_api_gateway.py index 4db08dd0890..3c959747daf 100644 --- a/tests/functional/event_handler/test_api_gateway.py +++ b/tests/functional/event_handler/test_api_gateway.py @@ -797,3 +797,29 @@ def foo(): # THEN a route for `/foo` should be found assert response["statusCode"] == 200 + + +@pytest.mark.parametrize( + "prefix", + [ + pytest.param("/foo", id="String are not supported"), + pytest.param({"/foo"}, id="Sets are not supported"), + pytest.param({"foo": "/foo"}, id="Dicts are not supported"), + pytest.param(tuple("/foo"), id="Tuples are not supported"), + pytest.param([None, 1, "", False], id="List of invalid values"), + ], +) +def test_ignore_invalid(prefix): + # GIVEN an invalid prefix + app = ApiGatewayResolver(strip_prefixes=prefix) + + @app.get("/foo/status") + def foo(): + ... + + # WHEN calling handler + response = app({"httpMethod": "GET", "path": "/foo/status"}, None) + + # THEN a route for `/foo/status` should be found + # so no prefix was stripped from the request path + assert response["statusCode"] == 200