Skip to content

Commit 1b5a469

Browse files
aahunghoffa
andauthored
fix: Raise correct exception when apigateway-endpoint-configuration is not a map (#2753)
Co-authored-by: Chris Rehn <[email protected]>
1 parent 5d90691 commit 1b5a469

9 files changed

+107
-16
lines changed

samtranslator/model/api/api_generator.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -324,7 +324,7 @@ def _add_endpoint_extension(self) -> None:
324324
self.logical_id, "DisableExecuteApiEndpoint works only within 'DefinitionBody' property."
325325
)
326326
editor = SwaggerEditor(self.definition_body)
327-
editor.add_disable_execute_api_endpoint_extension(self.disable_execute_api_endpoint) # type: ignore[no-untyped-call]
327+
editor.add_disable_execute_api_endpoint_extension(self.disable_execute_api_endpoint)
328328
self.definition_body = editor.swagger
329329

330330
def _construct_body_s3_dict(self) -> Dict[str, Any]:

samtranslator/model/apigateway.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44

55
from samtranslator.model import PropertyType, Resource
66
from samtranslator.model.exceptions import InvalidResourceException
7-
from samtranslator.model.types import IS_DICT, is_type, one_of, IS_STR, list_of
8-
from samtranslator.model.intrinsics import ref, fnSub
7+
from samtranslator.model.intrinsics import fnSub, ref
8+
from samtranslator.model.types import IS_DICT, IS_STR, is_type, list_of, one_of
99
from samtranslator.schema.common import PassThrough
1010
from samtranslator.translator import logical_id_generator
1111
from samtranslator.translator.arn_generator import ArnGenerator
@@ -32,7 +32,16 @@ class ApiGatewayRestApi(Resource):
3232

3333
Body: Optional[Dict[str, Any]]
3434
BodyS3Location: Optional[Dict[str, Any]]
35+
CloneFrom: Optional[PassThrough]
36+
Description: Optional[PassThrough]
37+
FailOnWarnings: Optional[PassThrough]
38+
Name: Optional[PassThrough]
39+
Parameters: Optional[Dict[str, Any]]
3540
EndpointConfiguration: Optional[Dict[str, Any]]
41+
BinaryMediaTypes: Optional[List[Any]]
42+
MinimumCompressionSize: Optional[PassThrough]
43+
Mode: Optional[PassThrough]
44+
ApiKeySourceType: Optional[PassThrough]
3645

3746
runtime_attrs = {"rest_api_id": lambda self: ref(self.logical_id)}
3847

samtranslator/swagger/swagger.py

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@
66
from samtranslator.model.intrinsics import ref, make_conditional, fnSub
77
from samtranslator.model.exceptions import InvalidDocumentException, InvalidTemplateException
88
from samtranslator.open_api.base_editor import BaseEditor
9+
from samtranslator.schema.common import PassThrough
910
from samtranslator.translator.arn_generator import ArnGenerator
1011
from samtranslator.utils.py27hash_fix import Py27Dict, Py27UniStr
12+
from samtranslator.utils.utils import InvalidValueType, dict_deep_set
1113

1214

1315
class SwaggerEditor(BaseEditor):
@@ -37,6 +39,7 @@ class SwaggerEditor(BaseEditor):
3739
_POLICY_TYPE_IAM = "Iam"
3840
_POLICY_TYPE_IP = "Ip"
3941
_POLICY_TYPE_VPC = "Vpc"
42+
_DISABLE_EXECUTE_API_ENDPOINT = "disableExecuteApiEndpoint"
4043

4144
def __init__(self, doc: Optional[Dict[str, Any]]) -> None:
4245
"""
@@ -66,32 +69,40 @@ def __init__(self, doc: Optional[Dict[str, Any]]) -> None:
6669
for path_item in self.get_conditional_contents(self.paths.get(path)):
6770
SwaggerEditor.validate_path_item_is_dict(path_item, path)
6871

69-
@staticmethod
70-
def _update_dict(obj: Dict[Any, Any], k: str, v: Dict[Any, Any]) -> None:
71-
if not obj.get(k):
72-
obj[k] = {}
73-
obj[k].update(v)
74-
75-
def add_disable_execute_api_endpoint_extension(self, disable_execute_api_endpoint): # type: ignore[no-untyped-def]
72+
def add_disable_execute_api_endpoint_extension(self, disable_execute_api_endpoint: PassThrough) -> None:
7673
"""Add endpoint configuration to _X_APIGW_ENDPOINT_CONFIG in open api definition as extension
7774
Following this guide:
7875
https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-swagger-extensions-endpoint-configuration.html
7976
:param boolean disable_execute_api_endpoint: Specifies whether clients can invoke your API by using the default execute-api endpoint.
8077
"""
81-
DISABLE_EXECUTE_API_ENDPOINT = "disableExecuteApiEndpoint"
82-
set_disable_api_endpoint = {DISABLE_EXECUTE_API_ENDPOINT: disable_execute_api_endpoint}
78+
79+
disable_execute_api_endpoint_path = f"{self._X_ENDPOINT_CONFIG}.{self._DISABLE_EXECUTE_API_ENDPOINT}"
8380

8481
# Check if the OpenAPI version is 3.0, if it is then the extension needs to added to the Servers field,
8582
# if not then it gets added to the top level (same level as "paths" and "info")
8683
if self._doc.get("openapi") and self.validate_open_api_version_3(self._doc["openapi"]):
8784
# Add the x-amazon-apigateway-endpoint-configuration extension to the Servers objects
8885
servers_configurations = self._doc.get(self._SERVERS, [Py27Dict()])
89-
for config in servers_configurations:
90-
SwaggerEditor._update_dict(config, self._X_ENDPOINT_CONFIG, set_disable_api_endpoint)
86+
for index, config in enumerate(servers_configurations):
87+
try:
88+
dict_deep_set(config, disable_execute_api_endpoint_path, disable_execute_api_endpoint)
89+
except InvalidValueType as ex:
90+
raise InvalidDocumentException(
91+
[
92+
InvalidTemplateException(
93+
f"Invalid OpenAPI definition of '{self._SERVERS}[{index}]': {str(ex)}."
94+
)
95+
]
96+
) from ex
9197

9298
self._doc[self._SERVERS] = servers_configurations
9399
else:
94-
SwaggerEditor._update_dict(self._doc, self._X_ENDPOINT_CONFIG, set_disable_api_endpoint)
100+
try:
101+
dict_deep_set(self._doc, disable_execute_api_endpoint_path, disable_execute_api_endpoint)
102+
except InvalidValueType as ex:
103+
raise InvalidDocumentException(
104+
[InvalidTemplateException(f"Invalid OpenAPI definition: {str(ex)}.")]
105+
) from ex
95106

96107
def add_lambda_integration(
97108
self,

samtranslator/utils/utils.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,25 @@ def dict_deep_get(d: Any, path: str) -> Optional[Any]:
4949
relative_path = (relative_path + f".{_path_nodes[0]}").lstrip(".")
5050
_path_nodes = _path_nodes[1:]
5151
return d
52+
53+
54+
def dict_deep_set(d: Any, path: str, value: Any) -> None:
55+
"""
56+
Set the value deep in the dict.
57+
58+
If any value along the path doesn't exist, set to {}.
59+
If any parent node exists but is not a dict, raise InvalidValueType.
60+
"""
61+
relative_path = ""
62+
if not path:
63+
raise ValueError("path cannot be empty")
64+
_path_nodes = path.split(".")
65+
while len(_path_nodes) > 1:
66+
if not isinstance(d, dict):
67+
raise InvalidValueType(relative_path)
68+
d = d.setdefault(_path_nodes[0], {})
69+
relative_path = (relative_path + f".{_path_nodes[0]}").lstrip(".")
70+
_path_nodes = _path_nodes[1:]
71+
if not isinstance(d, dict):
72+
raise InvalidValueType(relative_path)
73+
d[_path_nodes[0]] = value
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
Resources:
2+
ExplicitApi:
3+
Type: AWS::Serverless::Api
4+
Properties:
5+
StageName: SomeStage
6+
DefinitionBody:
7+
swagger: 2.0
8+
paths: {}
9+
x-amazon-apigateway-endpoint-configuration: this should be a dict
10+
DisableExecuteApiEndpoint: true
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
Resources:
2+
ExplicitApi:
3+
Type: AWS::Serverless::Api
4+
Properties:
5+
StageName: SomeStage
6+
DefinitionBody:
7+
openapi: 3.0.1
8+
paths: {}
9+
servers:
10+
- x-amazon-apigateway-endpoint-configuration:
11+
- this should be a dict
12+
DisableExecuteApiEndpoint: true
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 1. Structure of the SAM template is invalid. Invalid OpenAPI definition: The value of 'x-amazon-apigateway-endpoint-configuration' should be a map."
3+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 1. Structure of the SAM template is invalid. Invalid OpenAPI definition of 'servers[0]': The value of 'x-amazon-apigateway-endpoint-configuration' should be a map."
3+
}

tests/utils/test_utils.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from unittest import TestCase
22

3-
from samtranslator.utils.utils import as_array, insert_unique
3+
from samtranslator.utils.utils import InvalidValueType, as_array, dict_deep_set, insert_unique
44

55

66
class TestUtils(TestCase):
@@ -26,3 +26,24 @@ def test_insert_unique(self):
2626
ret = insert_unique(xs, vs)
2727
self.assertFalse(ret is xs)
2828
self.assertFalse(ret is vs)
29+
30+
def test_dict_deep_set(self):
31+
d = {"a": {"b": {"c": "hi"}}}
32+
dict_deep_set(d, "a.b.d.hello", "world")
33+
self.assertEqual(d, {"a": {"b": {"c": "hi", "d": {"hello": "world"}}}})
34+
dict_deep_set(d, "a.b.hello", "world")
35+
dict_deep_set(d, "a.hello", "world1")
36+
self.assertEqual(d, {"a": {"hello": "world1", "b": {"hello": "world", "c": "hi", "d": {"hello": "world"}}}})
37+
38+
def test_dict_deep_set_invalid_type(self):
39+
d = {"a": {"b": {"c": "hi"}}}
40+
with self.assertRaisesRegex(InvalidValueType, r"The value of 'a\.b\.c' should be a map"):
41+
dict_deep_set(d, "a.b.c.hello", "world")
42+
43+
with self.assertRaisesRegex(InvalidValueType, r"It should be a map"):
44+
dict_deep_set("a str", "a.b.c.hello", "world")
45+
46+
def test_dict_deep_set_invalid_path(self):
47+
d = {"a": {"b": {"c": "hi"}}}
48+
with self.assertRaisesRegex(ValueError, r"path cannot be empty"):
49+
dict_deep_set(d, "", "world")

0 commit comments

Comments
 (0)