Skip to content
Open
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
33 changes: 33 additions & 0 deletions samcli/lib/providers/sam_api_provider.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Parses SAM given the template"""

import logging
import re
from typing import Dict, List, Optional, Tuple, Union, cast

from samcli.commands.local.lib.swagger.integration_uri import LambdaUri
Expand All @@ -16,6 +17,11 @@

LOG = logging.getLogger(__name__)

# API Gateway REST API resource path parts are either literals made of these characters, or a path variable
# ({name} or greedy {name+}) that spans the whole path part. Per "Amazon API Gateway important notes for REST APIs",
# path segments may only contain alphanumeric characters, underscores, hyphens, periods, commas, colons and braces.
_REST_API_PATH_PART_PATTERN = re.compile(r"[a-zA-Z0-9._,:-]+|\{[a-zA-Z0-9._,:-]+\+?\}")


class SamApiProvider(CfnBaseApiProvider):
TYPES = [AWS_SERVERLESS_FUNCTION, AWS_SERVERLESS_API, AWS_SERVERLESS_HTTPAPI]
Expand Down Expand Up @@ -503,6 +509,9 @@ def _convert_event_route(
path = cast(str, event_properties.get(SamApiProvider._EVENT_PATH))
method = cast(str, event_properties.get(SamApiProvider._EVENT_METHOD))

if event_type == SamApiProvider._EVENT_TYPE_API and isinstance(path, str):
SamApiProvider._validate_rest_api_path(path, lambda_logical_id)

# An RESTAPI (HTTPAPI) Event, can have RestApiId (ApiId) property which designates the resource that owns this
# API. If omitted, the API is owned by Implicit API resource. This could either be a direct resource logical ID
# or a "Ref" of the logicalID
Expand Down Expand Up @@ -550,6 +559,30 @@ def _convert_event_route(
),
)

@staticmethod
def _validate_rest_api_path(path: str, lambda_logical_id: str) -> None:
"""
Validates the Path of an Api event against the resource path rules of API Gateway REST APIs,
so that paths which would fail on deployment are not served locally.

:param str path: Path of the Api event
:param str lambda_logical_id: Logical Id of the AWS::Serverless::Function
:raises InvalidSamDocumentException: if the path is not a valid API Gateway REST API path
"""
path_parts = [part for part in path.split("/") if part]
for index, part in enumerate(path_parts):
if not _REST_API_PATH_PART_PATTERN.fullmatch(part):
raise InvalidSamDocumentException(
f"Invalid Path '{path}' in Api event of function '{lambda_logical_id}': "
f"Resource's path part '{part}' only allows a-zA-Z0-9._,:- or a path variable "
"with curly braces at the beginning and the end and an optional plus sign before the closing brace."
)
if part.endswith("+}") and index != len(path_parts) - 1:
raise InvalidSamDocumentException(
f"Invalid Path '{path}' in Api event of function '{lambda_logical_id}': "
f"greedy path variable '{part}' must be the last part of the path."
)

@staticmethod
def merge_routes(collector: ApiCollector) -> List[Route]:
"""
Expand Down
41 changes: 41 additions & 0 deletions tests/unit/commands/local/lib/test_sam_api_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -2256,6 +2256,47 @@ def test_add_authorizer_in_serverless_function(self, authorizer_obj, expected_au
)


class TestSamApiRestApiPathValidation(TestCase):
@parameterized.expand(
[
("/",),
("/hello",),
("/users/{id}",),
("/{proxy+}",),
("/files/{name}/data.json",),
("/items/v1:batch-get",),
("/items/a,b",),
("/api/{version}/under_score/{proxy+}",),
("/trailing/slash/",),
]
)
def test_valid_rest_api_path(self, path):
_, route = SamApiProvider._convert_event_route("", "MyFunction", {"Path": path, "Method": "get"}, Route.API)

self.assertEqual(route.path, path)

@parameterized.expand(
[
("/api/map/{z}/{x}/{y}.png", "path part '{y}.png'"),
("/users/id-{id}", "path part 'id-{id}'"),
("/users/{first}{last}", "path part '{first}{last}'"),
("/hello world", "path part 'hello world'"),
("/{}", "path part '{}'"),
("/{proxy+}/items", "greedy path variable '{proxy\\+}' must be the last part"),
]
)
def test_invalid_rest_api_path_raises(self, path, expected_message):
with self.assertRaisesRegex(InvalidSamDocumentException, expected_message):
SamApiProvider._convert_event_route("", "MyFunction", {"Path": path, "Method": "get"}, Route.API)

def test_http_api_path_is_not_validated(self):
_, route = SamApiProvider._convert_event_route(
"", "MyFunction", {"Path": "/api/map/{y}.png", "Method": "get"}, Route.HTTP
)

self.assertEqual(route.path, "/api/map/{y}.png")


def make_swagger(routes, binary_media_types=None):
"""
Given a list of API configurations named tuples, returns a Swagger document
Expand Down