Skip to content

Commit 59df2db

Browse files
authored
handle 'Invalid Swagger Document' and refactor some validation into Swagger Editor constructor (#2263)
* handle 'Invalid Swagger Document' and refactor some validation into Swagger Editor constructor * missed removing a comment * move path item validation into method * update new function to use swagger editor instance * small change, changing test to use swagger instead of openapi * update deployment hashes in some updated tests * add docstring :raises
1 parent 7eb3587 commit 59df2db

30 files changed

+166
-159
lines changed

samtranslator/model/api/api_generator.py

Lines changed: 16 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,8 @@ def __init__(
236236
self.template_conditions = template_conditions
237237
self.mode = mode
238238

239+
self.swagger_editor = SwaggerEditor(self.definition_body) if self.definition_body else None
240+
239241
def _construct_rest_api(self):
240242
"""Constructs and returns the ApiGateway RestApi.
241243
@@ -282,7 +284,7 @@ def _construct_rest_api(self):
282284
rest_api.BodyS3Location = self._construct_body_s3_dict()
283285
elif self.definition_body:
284286
# # Post Process OpenApi Auth Settings
285-
self.definition_body = self._openapi_postprocess(self.definition_body)
287+
self.definition_body = self._openapi_postprocess(self.swagger_editor.swagger)
286288
rest_api.Body = self.definition_body
287289

288290
if self.name:
@@ -308,9 +310,7 @@ def _add_endpoint_extension(self):
308310
raise InvalidResourceException(
309311
self.logical_id, "DisableExecuteApiEndpoint works only within 'DefinitionBody' property."
310312
)
311-
editor = SwaggerEditor(self.definition_body)
312-
editor.add_disable_execute_api_endpoint_extension(self.disable_execute_api_endpoint)
313-
self.definition_body = editor.swagger
313+
self.swagger_editor.add_disable_execute_api_endpoint_extension(self.disable_execute_api_endpoint)
314314

315315
def _construct_body_s3_dict(self):
316316
"""Constructs the RestApi's `BodyS3Location property`_, from the SAM Api's DefinitionUri property.
@@ -620,13 +620,6 @@ def _add_cors(self):
620620
else:
621621
raise InvalidResourceException(self.logical_id, INVALID_ERROR)
622622

623-
if not SwaggerEditor.is_valid(self.definition_body):
624-
raise InvalidResourceException(
625-
self.logical_id,
626-
"Unable to add Cors configuration because "
627-
"'DefinitionBody' does not contain a valid Swagger definition.",
628-
)
629-
630623
if properties.AllowCredentials is True and properties.AllowOrigin == _CORS_WILDCARD:
631624
raise InvalidResourceException(
632625
self.logical_id,
@@ -635,10 +628,9 @@ def _add_cors(self):
635628
"'AllowOrigin' is \"'*'\" or not set",
636629
)
637630

638-
editor = SwaggerEditor(self.definition_body)
639-
for path in editor.iter_on_path():
631+
for path in self.swagger_editor.iter_on_path():
640632
try:
641-
editor.add_cors(
633+
self.swagger_editor.add_cors(
642634
path,
643635
properties.AllowOrigin,
644636
properties.AllowHeaders,
@@ -649,9 +641,6 @@ def _add_cors(self):
649641
except InvalidTemplateException as ex:
650642
raise InvalidResourceException(self.logical_id, ex.message)
651643

652-
# Assign the Swagger back to template
653-
self.definition_body = editor.swagger
654-
655644
def _add_binary_media_types(self):
656645
"""
657646
Add binary media types to Swagger
@@ -664,11 +653,7 @@ def _add_binary_media_types(self):
664653
if self.binary_media and not self.definition_body:
665654
return
666655

667-
editor = SwaggerEditor(self.definition_body)
668-
editor.add_binary_media_types(self.binary_media)
669-
670-
# Assign the Swagger back to template
671-
self.definition_body = editor.swagger
656+
self.swagger_editor.add_binary_media_types(self.binary_media)
672657

673658
def _add_auth(self):
674659
"""
@@ -687,40 +672,31 @@ def _add_auth(self):
687672
if not all(key in AuthProperties._fields for key in self.auth.keys()):
688673
raise InvalidResourceException(self.logical_id, "Invalid value for 'Auth' property")
689674

690-
if not SwaggerEditor.is_valid(self.definition_body):
691-
raise InvalidResourceException(
692-
self.logical_id,
693-
"Unable to add Auth configuration because "
694-
"'DefinitionBody' does not contain a valid Swagger definition.",
695-
)
696-
swagger_editor = SwaggerEditor(self.definition_body)
697675
auth_properties = AuthProperties(**self.auth)
698676
authorizers = self._get_authorizers(auth_properties.Authorizers, auth_properties.DefaultAuthorizer)
699677

700678
if authorizers:
701-
swagger_editor.add_authorizers_security_definitions(authorizers)
679+
self.swagger_editor.add_authorizers_security_definitions(authorizers)
702680
self._set_default_authorizer(
703-
swagger_editor,
681+
self.swagger_editor,
704682
authorizers,
705683
auth_properties.DefaultAuthorizer,
706684
auth_properties.AddDefaultAuthorizerToCorsPreflight,
707685
auth_properties.Authorizers,
708686
)
709687

710688
if auth_properties.ApiKeyRequired:
711-
swagger_editor.add_apikey_security_definition()
712-
self._set_default_apikey_required(swagger_editor)
689+
self.swagger_editor.add_apikey_security_definition()
690+
self._set_default_apikey_required(self.swagger_editor)
713691

714692
if auth_properties.ResourcePolicy:
715693
SwaggerEditor.validate_is_dict(
716694
auth_properties.ResourcePolicy, "ResourcePolicy must be a map (ResourcePolicyStatement)."
717695
)
718-
for path in swagger_editor.iter_on_path():
719-
swagger_editor.add_resource_policy(auth_properties.ResourcePolicy, path, self.stage_name)
696+
for path in self.swagger_editor.iter_on_path():
697+
self.swagger_editor.add_resource_policy(auth_properties.ResourcePolicy, path, self.stage_name)
720698
if auth_properties.ResourcePolicy.get("CustomStatements"):
721-
swagger_editor.add_custom_statements(auth_properties.ResourcePolicy.get("CustomStatements"))
722-
723-
self.definition_body = self._openapi_postprocess(swagger_editor.swagger)
699+
self.swagger_editor.add_custom_statements(auth_properties.ResourcePolicy.get("CustomStatements"))
724700

725701
def _construct_usage_plan(self, rest_api_stage=None):
726702
"""Constructs and returns the ApiGateway UsagePlan, ApiGateway UsagePlanKey, ApiGateway ApiKey for Auth.
@@ -923,15 +899,6 @@ def _add_gateway_responses(self):
923899
),
924900
)
925901

926-
if not SwaggerEditor.is_valid(self.definition_body):
927-
raise InvalidResourceException(
928-
self.logical_id,
929-
"Unable to add Auth configuration because "
930-
"'DefinitionBody' does not contain a valid Swagger definition.",
931-
)
932-
933-
swagger_editor = SwaggerEditor(self.definition_body)
934-
935902
# The dicts below will eventually become part of swagger/openapi definition, thus requires using Py27Dict()
936903
gateway_responses = Py27Dict()
937904
for response_type, response in self.gateway_responses.items():
@@ -943,10 +910,7 @@ def _add_gateway_responses(self):
943910
)
944911

945912
if gateway_responses:
946-
swagger_editor.add_gateway_responses(gateway_responses)
947-
948-
# Assign the Swagger back to template
949-
self.definition_body = swagger_editor.swagger
913+
self.swagger_editor.add_gateway_responses(gateway_responses)
950914

951915
def _add_models(self):
952916
"""
@@ -962,22 +926,10 @@ def _add_models(self):
962926
self.logical_id, "Models works only with inline Swagger specified in " "'DefinitionBody' property."
963927
)
964928

965-
if not SwaggerEditor.is_valid(self.definition_body):
966-
raise InvalidResourceException(
967-
self.logical_id,
968-
"Unable to add Models definitions because "
969-
"'DefinitionBody' does not contain a valid Swagger definition.",
970-
)
971-
972929
if not all(isinstance(model, dict) for model in self.models.values()):
973930
raise InvalidResourceException(self.logical_id, "Invalid value for 'Models' property")
974931

975-
swagger_editor = SwaggerEditor(self.definition_body)
976-
swagger_editor.add_models(self.models)
977-
978-
# Assign the Swagger back to template
979-
980-
self.definition_body = self._openapi_postprocess(swagger_editor.swagger)
932+
self.swagger_editor.add_models(self.models)
981933

982934
def _openapi_postprocess(self, definition_body):
983935
"""

samtranslator/swagger/swagger.py

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -46,19 +46,13 @@ def __init__(self, doc):
4646
modifications on this copy.
4747
4848
:param dict doc: Swagger document as a dictionary
49-
:raises ValueError: If the input Swagger document does not meet the basic Swagger requirements.
49+
:raises InvalidDocumentException if doc is invalid
5050
"""
5151

52-
if not SwaggerEditor.is_valid(doc):
53-
raise ValueError("Invalid Swagger document")
54-
5552
self._doc = copy.deepcopy(doc)
56-
self.paths = self._doc["paths"]
57-
self.security_definitions = self._doc.get("securityDefinitions", Py27Dict())
58-
self.gateway_responses = self._doc.get(self._X_APIGW_GATEWAY_RESPONSES, Py27Dict())
59-
self.resource_policy = self._doc.get(self._X_APIGW_POLICY, Py27Dict())
60-
self.definitions = self._doc.get("definitions", Py27Dict())
53+
self.validate_definition_body(doc)
6154

55+
self.paths = self._doc.get("paths")
6256
# https://swagger.io/specification/#path-item-object
6357
# According to swagger spec,
6458
# each path item object must be a dict (even it is empty).
@@ -67,6 +61,11 @@ def __init__(self, doc):
6761
for path in self.iter_on_path():
6862
SwaggerEditor.validate_path_item_is_dict(self.get_path(path), path)
6963

64+
self.security_definitions = self._doc.get("securityDefinitions", Py27Dict())
65+
self.gateway_responses = self._doc.get(self._X_APIGW_GATEWAY_RESPONSES, Py27Dict())
66+
self.resource_policy = self._doc.get(self._X_APIGW_POLICY, Py27Dict())
67+
self.definitions = self._doc.get("definitions", Py27Dict())
68+
7069
def get_path(self, path):
7170
path_dict = self.paths.get(path)
7271
if isinstance(path_dict, dict) and self._CONDITIONAL_IF in path_dict:
@@ -162,7 +161,6 @@ def add_path(self, path, method=None):
162161
163162
:param string path: Path name
164163
:param string method: HTTP method
165-
:raises ValueError: If the value of `path` in Swagger is not a dictionary
166164
"""
167165
method = self._normalize_method_name(method)
168166

@@ -1310,6 +1308,32 @@ def is_valid(data):
13101308
)
13111309
return False
13121310

1311+
def validate_definition_body(self, definition_body):
1312+
"""
1313+
Checks if definition_body is a valid Swagger document
1314+
1315+
:param dict definition_body: Data to be validated
1316+
:raises InvalidDocumentException if definition_body is invalid
1317+
"""
1318+
1319+
SwaggerEditor.validate_is_dict(definition_body, "DefinitionBody must be a dictionary.")
1320+
SwaggerEditor.validate_is_dict(
1321+
definition_body.get("paths"), "The 'paths' property of DefinitionBody must be present and be a dictionary."
1322+
)
1323+
1324+
has_swagger = definition_body.get("swagger")
1325+
has_openapi3 = definition_body.get("openapi") and SwaggerEditor.safe_compare_regex_with_string(
1326+
SwaggerEditor.get_openapi_version_3_regex(), definition_body["openapi"]
1327+
)
1328+
if not (has_swagger) and not (has_openapi3):
1329+
raise InvalidDocumentException(
1330+
[
1331+
InvalidTemplateException(
1332+
"DefinitionBody must have either: (1) a 'swagger' property or (2) an 'openapi' property with version 3.x or 3.x.x"
1333+
)
1334+
]
1335+
)
1336+
13131337
@staticmethod
13141338
def validate_is_dict(obj, exception_message):
13151339
"""

tests/model/api/test_api_generator.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ def test_construct_usage_plan_with_invalid_usage_plan_type(self, invalid_usage_p
1818
Mock(),
1919
Mock(),
2020
Mock(),
21-
Mock(),
21+
{"paths": {}, "openapi": "3.0.1"},
2222
Mock(),
2323
Mock(),
2424
Mock(),
@@ -39,7 +39,7 @@ def test_construct_usage_plan_with_invalid_usage_plan_fields(self, AuthPropertie
3939
Mock(),
4040
Mock(),
4141
Mock(),
42-
Mock(),
42+
{"paths": {}, "openapi": "3.0.1"},
4343
Mock(),
4444
Mock(),
4545
Mock(),

tests/swagger/test_swagger.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ class TestSwaggerEditor_init(TestCase):
1818
def test_must_raise_on_invalid_swagger(self):
1919

2020
invalid_swagger = {"paths": {}} # Missing "Swagger" keyword
21-
with self.assertRaises(ValueError):
21+
with self.assertRaises(InvalidDocumentException):
2222
SwaggerEditor(invalid_swagger)
2323

2424
def test_must_succeed_on_valid_swagger(self):
@@ -32,13 +32,13 @@ def test_must_succeed_on_valid_swagger(self):
3232
def test_must_fail_on_invalid_openapi_version(self):
3333
invalid_swagger = {"openapi": "2.3.0", "paths": {"/foo": {}, "/bar": {}}}
3434

35-
with self.assertRaises(ValueError):
35+
with self.assertRaises(InvalidDocumentException):
3636
SwaggerEditor(invalid_swagger)
3737

3838
def test_must_fail_on_invalid_openapi_version_2(self):
3939
invalid_swagger = {"openapi": "3.1.1.1", "paths": {"/foo": {}, "/bar": {}}}
4040

41-
with self.assertRaises(ValueError):
41+
with self.assertRaises(InvalidDocumentException):
4242
SwaggerEditor(invalid_swagger)
4343

4444
def test_must_succeed_on_valid_openapi3(self):
@@ -53,7 +53,7 @@ def test_must_succeed_on_valid_openapi3(self):
5353
def test_must_fail_with_bad_values_for_path(self, invalid_path_item):
5454
invalid_swagger = {"openapi": "3.1.1.1", "paths": {"/foo": {}, "/bad": invalid_path_item}}
5555

56-
with self.assertRaises(ValueError):
56+
with self.assertRaises(InvalidDocumentException):
5757
SwaggerEditor(invalid_swagger)
5858

5959

tests/translator/input/api_with_resource_refs.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ Resources:
66
Properties:
77
StageName: foo
88
DefinitionBody:
9-
"this": "is"
10-
"a": "swagger"
9+
paths: {}
10+
openapi: "3.0.1"
1111

1212
MyFunction:
1313
Type: "AWS::Serverless::Function"
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
Resources:
2+
MyApi:
3+
Type: AWS::Serverless::Api
4+
Properties:
5+
StageName: Prod
6+
DefinitionBody:
7+
info:
8+
version: '1.0'
9+
title: 'title'
10+
paths:
11+
"/some/path": {}
12+
"/other": {}
13+
openapi: '2.0'
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
Resources:
2+
MyApi:
3+
Type: AWS::Serverless::Api
4+
Properties:
5+
StageName: Prod
6+
DefinitionBody:
7+
info:
8+
version: '1.0'
9+
title: 'title'
10+
openapi: 3.0.1
11+
paths: 'invalid'
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
Resources:
2+
MyApi:
3+
Type: AWS::Serverless::Api
4+
Properties:
5+
StageName: Prod
6+
DefinitionBody:
7+
info:
8+
version: '1.0'
9+
title: 'title'
10+
paths:
11+
"/some/path": {}
12+
"/other": {}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
Resources:
2+
MyApi:
3+
Type: AWS::Serverless::Api
4+
Properties:
5+
StageName: Prod
6+
DefinitionBody:
7+
info:
8+
version: '1.0'
9+
title: 'title'
10+
openapi: 3.0.1

tests/translator/input/error_api_invalid_auth.yaml

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -121,14 +121,6 @@ Resources:
121121
Auth:
122122
MyBad: Foo
123123

124-
AuthWithInvalidDefinitionBodyApi:
125-
Type: AWS::Serverless::Api
126-
Properties:
127-
StageName: Prod
128-
DefinitionBody: { invalid: true }
129-
Auth:
130-
DefaultAuthorizer: Foo
131-
132124
AuthWithMissingDefaultAuthorizerApi:
133125
Type: AWS::Serverless::Api
134126
Properties:

0 commit comments

Comments
 (0)