From bff7fec4e43d13157a9c156c6a394acbd44d5cee Mon Sep 17 00:00:00 2001 From: Chih-Hsuan Yen Date: Wed, 17 Mar 2021 13:37:29 +0800 Subject: [PATCH 01/16] chore: don't install integration tests (#1964) --- setup.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 38af9df29..8ca3f5605 100755 --- a/setup.py +++ b/setup.py @@ -58,7 +58,9 @@ def read_requirements(req="base.txt"): url="https://github.com/awslabs/serverless-application-model", license="Apache License 2.0", # Exclude all but the code folders - packages=find_packages(exclude=("tests", "tests.*", "docs", "examples", "versions")), + packages=find_packages( + exclude=("tests", "tests.*", "integration", "integration.*", "docs", "examples", "versions") + ), install_requires=read_requirements("base.txt"), include_package_data=True, extras_require={"dev": read_requirements("dev.txt")}, From e259b15fca3ff6ea3c79d3959eefc15170b067ea Mon Sep 17 00:00:00 2001 From: Harsh Mishra Date: Wed, 17 Mar 2021 11:07:41 +0530 Subject: [PATCH 02/16] Remove unnecessary use of comprehension (#1805) --- tests/openapi/test_openapi.py | 2 +- tests/swagger/test_swagger.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/openapi/test_openapi.py b/tests/openapi/test_openapi.py index f29cb4d47..99197de9c 100644 --- a/tests/openapi/test_openapi.py +++ b/tests/openapi/test_openapi.py @@ -292,7 +292,7 @@ def setUp(self): def test_must_iterate_on_paths(self): expected = {"/foo", "/bar", "/baz"} - actual = set([path for path in self.editor.iter_on_path()]) + actual = set(list(self.editor.iter_on_path())) self.assertEqual(expected, actual) diff --git a/tests/swagger/test_swagger.py b/tests/swagger/test_swagger.py index 615fabec3..cf6147726 100644 --- a/tests/swagger/test_swagger.py +++ b/tests/swagger/test_swagger.py @@ -302,7 +302,7 @@ def setUp(self): def test_must_iterate_on_paths(self): expected = {"/foo", "/bar", "/baz"} - actual = set([path for path in self.editor.iter_on_path()]) + actual = set(list(self.editor.iter_on_path())) self.assertEqual(expected, actual) From 5698a15d701df2ce6a00d09b3a023458e7aad4dd Mon Sep 17 00:00:00 2001 From: Pranav <54665036+Pranav016@users.noreply.github.com> Date: Thu, 18 Mar 2021 13:10:05 +0530 Subject: [PATCH 03/16] fix: Grammatical error in README.md (#1965) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 09e8c5638..c2d6f6b70 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ environment that lets you locally build, test, debug, and deploy applications de ## What is this Github repository? 💻 -This GitHub repository contains the SAM Specification, the Python code that translates SAM templates into AWS CloudFormation stacks and lots of examples applications. +This GitHub repository contains the SAM Specification, the Python code that translates SAM templates into AWS CloudFormation stacks and lots of example applications. In the words of SAM developers: > SAM Translator is the Python code that deploys SAM templates via AWS CloudFormation. Source code is high quality (95% unit test coverage), From 4ac29da017e7a1108bffa5e068e94348f4f2d786 Mon Sep 17 00:00:00 2001 From: Cosh_ Date: Mon, 29 Mar 2021 09:37:25 -0700 Subject: [PATCH 04/16] fix: Added SAR Support Check (#1972) * Added SAR Support Check * Added docstring and Removed Instance Initialization for Class Method --- .../plugins/application/serverless_app_plugin.py | 5 +++++ samtranslator/region_configuration.py | 14 ++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/samtranslator/plugins/application/serverless_app_plugin.py b/samtranslator/plugins/application/serverless_app_plugin.py index 2f4d9b0e4..02333e0f4 100644 --- a/samtranslator/plugins/application/serverless_app_plugin.py +++ b/samtranslator/plugins/application/serverless_app_plugin.py @@ -12,6 +12,7 @@ from samtranslator.public.sdk.template import SamTemplate from samtranslator.intrinsics.resolver import IntrinsicsResolver from samtranslator.intrinsics.actions import FindInMapAction +from samtranslator.region_configuration import RegionConfiguration LOG = logging.getLogger(__name__) @@ -104,6 +105,10 @@ def on_before_transform_template(self, template_dict): if key not in self._applications: try: + if not RegionConfiguration.is_sar_supported(): + raise InvalidResourceException( + logical_id, "Serverless Application Repository is not available in this region." + ) # Lazy initialization of the client- create it when it is needed if not self._sar_client: self._sar_client = boto3.client("serverlessrepo") diff --git a/samtranslator/region_configuration.py b/samtranslator/region_configuration.py index 712d4faf9..c7c6b55e8 100644 --- a/samtranslator/region_configuration.py +++ b/samtranslator/region_configuration.py @@ -1,3 +1,5 @@ +import boto3 + from .translator.arn_generator import ArnGenerator @@ -22,3 +24,15 @@ def is_apigw_edge_configuration_supported(cls): "aws-iso-b", "aws-cn", ] + + @classmethod + def is_sar_supported(cls): + """ + SAR is not supported in af-south-1 at the moment. + https://aws.amazon.com/about-aws/global-infrastructure/regional-product-services/ + + :return: True, if SAR is supported in current region. + """ + return boto3.Session().region_name not in [ + "af-south-1", + ] From 701e3d1ae6b665d03d69cd1c03fac2ab572cbe63 Mon Sep 17 00:00:00 2001 From: Mohamed Elasmar <71043312+moelasmar@users.noreply.github.com> Date: Tue, 30 Mar 2021 16:40:34 -0700 Subject: [PATCH 05/16] update pyyaml version to get the security update (#1974) --- requirements/dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/dev.txt b/requirements/dev.txt index be3851301..193b157bb 100644 --- a/requirements/dev.txt +++ b/requirements/dev.txt @@ -3,7 +3,7 @@ flake8~=3.8.4 tox~=3.20.1 pytest-cov~=2.10.1 pylint>=1.7.2,<2.0 -pyyaml~=5.3.1 +pyyaml~=5.4 # Test requirements pytest~=6.1.1; python_version >= '3.6' From dc7413d23a93a8d5da01bb94df9e0abd09fd7393 Mon Sep 17 00:00:00 2001 From: daftster Date: Tue, 6 Apr 2021 15:24:52 -0400 Subject: [PATCH 06/16] Issue 1508 remove check requiring identity to be required if ReauthorizeEvery equals zero (#1577) * remove check requiring identity to be required Check removed to avoid must specify Identity with at least one of Headers, QueryStrings, StageVariables, or Context. error. This is allowed to be removed from aws console. * set identity to empty dictionary Revert back removal of code section and set identity to empty dictionary instead when function_payload_type is "REQUEST" and no identity defined. * use the correct identity variable fix issue catched by unit test. * Update apigateway.py just set the identity to None * undo change. * remove extra spaces * remove some more spaces * Update test_translator.py remove from test case error_api_invalid_auth as this should be valid. * make the Lambda Authorizer is optional if the authorization caching is not enabled (reference https://docs.aws.amazon.com/apigateway/api-reference/resource/authorizer/#identitySource) * add unit testing to cover the InvalidResourceException in case if the identity values are not exist, and not cached * black reformat Co-authored-by: Mohamed Elasmar --- samtranslator/model/apigateway.py | 7 +- tests/model/test_api.py | 15 ++ .../input/api_with_auth_all_minimum.yaml | 20 +++ .../output/api_with_auth_all_minimum.json | 132 ++++++++++++++++- .../aws-cn/api_with_auth_all_minimum.json | 138 +++++++++++++++++- .../aws-us-gov/api_with_auth_all_minimum.json | 136 ++++++++++++++++- 6 files changed, 437 insertions(+), 11 deletions(-) diff --git a/samtranslator/model/apigateway.py b/samtranslator/model/apigateway.py index 649f277c6..607c38529 100644 --- a/samtranslator/model/apigateway.py +++ b/samtranslator/model/apigateway.py @@ -267,8 +267,9 @@ def _is_missing_identity_source(self, identity): query_strings = identity.get("QueryStrings") stage_variables = identity.get("StageVariables") context = identity.get("Context") + ttl = identity.get("ReauthorizeEvery") - if not headers and not query_strings and not stage_variables and not context: + if (ttl is None or int(ttl) > 0) and not headers and not query_strings and not stage_variables and not context: return True return False @@ -311,7 +312,9 @@ def generate_swagger(self): swagger[APIGATEWAY_AUTHORIZER_KEY]["authorizerCredentials"] = function_invoke_role if self._get_function_payload_type() == "REQUEST": - swagger[APIGATEWAY_AUTHORIZER_KEY]["identitySource"] = self._get_identity_source() + identity_source = self._get_identity_source() + if identity_source: + swagger[APIGATEWAY_AUTHORIZER_KEY]["identitySource"] = self._get_identity_source() # Authorizer Validation Expression is only allowed on COGNITO_USER_POOLS and LAMBDA_TOKEN is_lambda_token_authorizer = authorizer_type == "LAMBDA" and self._get_function_payload_type() == "TOKEN" diff --git a/tests/model/test_api.py b/tests/model/test_api.py index 627bda3d5..8bc871ee7 100644 --- a/tests/model/test_api.py +++ b/tests/model/test_api.py @@ -17,3 +17,18 @@ def test_create_authorizer_fails_with_string_authorization_scopes(self): auth = ApiGatewayAuthorizer( api_logical_id="logicalId", name="authName", authorization_scopes="invalid_scope" ) + + def test_create_authorizer_fails_with_missing_identity_values_and_not_cached(self): + with pytest.raises(InvalidResourceException): + auth = ApiGatewayAuthorizer( + api_logical_id="logicalId", + name="authName", + identity={"ReauthorizeEvery": 10}, + function_payload_type="REQUEST", + ) + + def test_create_authorizer_fails_with_empty_identity(self): + with pytest.raises(InvalidResourceException): + auth = ApiGatewayAuthorizer( + api_logical_id="logicalId", name="authName", identity={}, function_payload_type="REQUEST" + ) diff --git a/tests/translator/input/api_with_auth_all_minimum.yaml b/tests/translator/input/api_with_auth_all_minimum.yaml index f6eda0af2..b89479219 100644 --- a/tests/translator/input/api_with_auth_all_minimum.yaml +++ b/tests/translator/input/api_with_auth_all_minimum.yaml @@ -32,6 +32,20 @@ Resources: Identity: Headers: - Authorization1 + + MyApiWithNotCachedLambdaRequestAuth: + Type: "AWS::Serverless::Api" + Properties: + StageName: Prod + Auth: + DefaultAuthorizer: MyLambdaRequestAuth + Authorizers: + MyLambdaRequestAuth: + FunctionPayloadType: REQUEST + FunctionArn: !GetAtt MyAuthFn.Arn + Identity: + ReauthorizeEvery: 0 + MyAuthFn: Type: AWS::Serverless::Function Properties: @@ -63,6 +77,12 @@ Resources: RestApiId: !Ref MyApiWithLambdaRequestAuth Method: get Path: /lambda-request + LambdaNotCachedRequest: + Type: Api + Properties: + RestApiId: !Ref MyApiWithNotCachedLambdaRequestAuth + Method: get + Path: /not-cached-lambda-request MyUserPool: Type: AWS::Cognito::UserPool Properties: diff --git a/tests/translator/output/api_with_auth_all_minimum.json b/tests/translator/output/api_with_auth_all_minimum.json index 640d74f16..4a4dbe937 100644 --- a/tests/translator/output/api_with_auth_all_minimum.json +++ b/tests/translator/output/api_with_auth_all_minimum.json @@ -63,7 +63,19 @@ }, "StageName": "Prod" } - }, + }, + "MyApiWithNotCachedLambdaRequestAuthProdStage": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "DeploymentId": { + "Ref": "MyApiWithNotCachedLambdaRequestAuthDeployment444f67cd7c" + }, + "RestApiId": { + "Ref": "MyApiWithNotCachedLambdaRequestAuth" + }, + "StageName": "Prod" + } + }, "MyApiWithLambdaTokenAuthMyLambdaTokenAuthAuthorizerPermission": { "Type": "AWS::Lambda::Permission", "Properties": { @@ -205,7 +217,30 @@ ] } } - }, + }, + "MyApiWithNotCachedLambdaRequestAuthMyLambdaRequestAuthAuthorizerPermission": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "Principal": "apigateway.amazonaws.com", + "FunctionName": { + "Fn::GetAtt": [ + "MyAuthFn", + "Arn" + ] + }, + "SourceArn": { + "Fn::Sub": [ + "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${__ApiId__}/authorizers/*", + { + "__ApiId__": { + "Ref": "MyApiWithNotCachedLambdaRequestAuth" + } + } + ] + } + } + }, "MyFnLambdaTokenPermissionProd": { "Type": "AWS::Lambda::Permission", "Properties": { @@ -236,7 +271,17 @@ "Description": "RestApi deployment id: 6e52add211cda52ae10a7cc0e0afcf4afc682f9f", "StageName": "Stage" } - }, + }, + "MyApiWithNotCachedLambdaRequestAuthDeployment444f67cd7c": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "RestApiId": { + "Ref": "MyApiWithNotCachedLambdaRequestAuth" + }, + "Description": "RestApi deployment id: 444f67cd7c6475a698a0101480ba99b498325e90", + "StageName": "Stage" + } + }, "MyFnLambdaRequestPermissionProd": { "Type": "AWS::Lambda::Permission", "Properties": { @@ -257,7 +302,28 @@ ] } } - }, + }, + "MyFnLambdaNotCachedRequestPermissionProd": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "Principal": "apigateway.amazonaws.com", + "FunctionName": { + "Ref": "MyFn" + }, + "SourceArn": { + "Fn::Sub": [ + "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${__ApiId__}/${__Stage__}/GET/not-cached-lambda-request", + { + "__Stage__": "*", + "__ApiId__": { + "Ref": "MyApiWithNotCachedLambdaRequestAuth" + } + } + ] + } + } + }, "MyApiWithLambdaTokenAuth": { "Type": "AWS::ApiGateway::RestApi", "Properties": { @@ -468,6 +534,64 @@ } } } + }, + "MyApiWithNotCachedLambdaRequestAuth": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "Body": { + "info": { + "version": "1.0", + "title": { + "Ref": "AWS::StackName" + } + }, + "paths": { + "/not-cached-lambda-request": { + "get": { + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "type": "aws_proxy", + "uri": { + "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${MyFn.Arn}/invocations" + } + }, + "security": [ + { + "MyLambdaRequestAuth": [] + } + ], + "responses": {} + } + } + }, + "swagger": "2.0", + "securityDefinitions": { + "MyLambdaRequestAuth": { + "in": "header", + "type": "apiKey", + "name": "Unused", + "x-amazon-apigateway-authorizer": { + "type": "request", + "authorizerUri": { + "Fn::Sub": [ + "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${__FunctionArn__}/invocations", + { + "__FunctionArn__": { + "Fn::GetAtt": [ + "MyAuthFn", + "Arn" + ] + } + } + ] + }, + "authorizerResultTtlInSeconds": 0 + }, + "x-amazon-apigateway-authtype": "custom" + } + } + } + } } } } diff --git a/tests/translator/output/aws-cn/api_with_auth_all_minimum.json b/tests/translator/output/aws-cn/api_with_auth_all_minimum.json index f568fdaa8..c072d8df8 100644 --- a/tests/translator/output/aws-cn/api_with_auth_all_minimum.json +++ b/tests/translator/output/aws-cn/api_with_auth_all_minimum.json @@ -71,6 +71,18 @@ }, "StageName": "Prod" } + }, + "MyApiWithNotCachedLambdaRequestAuthProdStage": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "DeploymentId": { + "Ref": "MyApiWithNotCachedLambdaRequestAuthDeployment234e92eab4" + }, + "RestApiId": { + "Ref": "MyApiWithNotCachedLambdaRequestAuth" + }, + "StageName": "Prod" + } }, "MyApiWithLambdaTokenAuthMyLambdaTokenAuthAuthorizerPermission": { "Type": "AWS::Lambda::Permission", @@ -203,7 +215,30 @@ ] } } - }, + }, + "MyApiWithNotCachedLambdaRequestAuthMyLambdaRequestAuthAuthorizerPermission": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "Principal": "apigateway.amazonaws.com", + "FunctionName": { + "Fn::GetAtt": [ + "MyAuthFn", + "Arn" + ] + }, + "SourceArn": { + "Fn::Sub": [ + "arn:aws-cn:execute-api:${AWS::Region}:${AWS::AccountId}:${__ApiId__}/authorizers/*", + { + "__ApiId__": { + "Ref": "MyApiWithNotCachedLambdaRequestAuth" + } + } + ] + } + } + }, "MyApiWithLambdaTokenAuthDeploymenta48b731095": { "Type": "AWS::ApiGateway::Deployment", "Properties": { @@ -213,7 +248,17 @@ "Description": "RestApi deployment id: a48b7310952ed029bd212c380e89a1bd39c74eae", "StageName": "Stage" } - }, + }, + "MyApiWithNotCachedLambdaRequestAuthDeployment234e92eab4": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "RestApiId": { + "Ref": "MyApiWithNotCachedLambdaRequestAuth" + }, + "Description": "RestApi deployment id: 234e92eab4e4c590ad261ddd55775c1edcc2972f", + "StageName": "Stage" + } + }, "MyFnLambdaTokenPermissionProd": { "Type": "AWS::Lambda::Permission", "Properties": { @@ -255,7 +300,28 @@ ] } } - }, + }, + "MyFnLambdaNotCachedRequestPermissionProd": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "Principal": "apigateway.amazonaws.com", + "FunctionName": { + "Ref": "MyFn" + }, + "SourceArn": { + "Fn::Sub": [ + "arn:aws-cn:execute-api:${AWS::Region}:${AWS::AccountId}:${__ApiId__}/${__Stage__}/GET/not-cached-lambda-request", + { + "__Stage__": "*", + "__ApiId__": { + "Ref": "MyApiWithNotCachedLambdaRequestAuth" + } + } + ] + } + } + }, "MyApiWithLambdaTokenAuth": { "Type": "AWS::ApiGateway::RestApi", "Properties": { @@ -492,6 +558,72 @@ "endpointConfigurationTypes": "REGIONAL" } } + }, + "MyApiWithNotCachedLambdaRequestAuth": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "Body": { + "info": { + "version": "1.0", + "title": { + "Ref": "AWS::StackName" + } + }, + "paths": { + "/not-cached-lambda-request": { + "get": { + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "type": "aws_proxy", + "uri": { + "Fn::Sub": "arn:aws-cn:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${MyFn.Arn}/invocations" + } + }, + "security": [ + { + "MyLambdaRequestAuth": [] + } + ], + "responses": {} + } + } + }, + "swagger": "2.0", + "securityDefinitions": { + "MyLambdaRequestAuth": { + "in": "header", + "type": "apiKey", + "name": "Unused", + "x-amazon-apigateway-authorizer": { + "type": "request", + "authorizerUri": { + "Fn::Sub": [ + "arn:aws-cn:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${__FunctionArn__}/invocations", + { + "__FunctionArn__": { + "Fn::GetAtt": [ + "MyAuthFn", + "Arn" + ] + } + } + ] + }, + "authorizerResultTtlInSeconds": 0 + }, + "x-amazon-apigateway-authtype": "custom" + } + } + }, + "EndpointConfiguration": { + "Types": [ + "REGIONAL" + ] + }, + "Parameters": { + "endpointConfigurationTypes": "REGIONAL" + } + } } } } diff --git a/tests/translator/output/aws-us-gov/api_with_auth_all_minimum.json b/tests/translator/output/aws-us-gov/api_with_auth_all_minimum.json index b108f7b43..7ab9dea6e 100644 --- a/tests/translator/output/aws-us-gov/api_with_auth_all_minimum.json +++ b/tests/translator/output/aws-us-gov/api_with_auth_all_minimum.json @@ -71,6 +71,18 @@ }, "StageName": "Prod" } + }, + "MyApiWithNotCachedLambdaRequestAuthProdStage": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "DeploymentId": { + "Ref": "MyApiWithNotCachedLambdaRequestAuthDeploymentd3b8858811" + }, + "RestApiId": { + "Ref": "MyApiWithNotCachedLambdaRequestAuth" + }, + "StageName": "Prod" + } }, "MyApiWithLambdaTokenAuthMyLambdaTokenAuthAuthorizerPermission": { "Type": "AWS::Lambda::Permission", @@ -213,7 +225,30 @@ ] } } - }, + }, + "MyApiWithNotCachedLambdaRequestAuthMyLambdaRequestAuthAuthorizerPermission": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "Principal": "apigateway.amazonaws.com", + "FunctionName": { + "Fn::GetAtt": [ + "MyAuthFn", + "Arn" + ] + }, + "SourceArn": { + "Fn::Sub": [ + "arn:aws-us-gov:execute-api:${AWS::Region}:${AWS::AccountId}:${__ApiId__}/authorizers/*", + { + "__ApiId__": { + "Ref": "MyApiWithNotCachedLambdaRequestAuth" + } + } + ] + } + } + }, "MyFnLambdaTokenPermissionProd": { "Type": "AWS::Lambda::Permission", "Properties": { @@ -234,7 +269,17 @@ ] } } - }, + }, + "MyApiWithNotCachedLambdaRequestAuthDeploymentd3b8858811": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "RestApiId": { + "Ref": "MyApiWithNotCachedLambdaRequestAuth" + }, + "Description": "RestApi deployment id: d3b8858811d6c42be45490ba4d1ca059821cf4fd", + "StageName": "Stage" + } + }, "MyFnLambdaRequestPermissionProd": { "Type": "AWS::Lambda::Permission", "Properties": { @@ -256,6 +301,27 @@ } } }, + "MyFnLambdaNotCachedRequestPermissionProd": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "Principal": "apigateway.amazonaws.com", + "FunctionName": { + "Ref": "MyFn" + }, + "SourceArn": { + "Fn::Sub": [ + "arn:aws-us-gov:execute-api:${AWS::Region}:${AWS::AccountId}:${__ApiId__}/${__Stage__}/GET/not-cached-lambda-request", + { + "__Stage__": "*", + "__ApiId__": { + "Ref": "MyApiWithNotCachedLambdaRequestAuth" + } + } + ] + } + } + }, "MyApiWithLambdaTokenAuth": { "Type": "AWS::ApiGateway::RestApi", "Properties": { @@ -492,6 +558,72 @@ "endpointConfigurationTypes": "REGIONAL" } } + }, + "MyApiWithNotCachedLambdaRequestAuth": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "Body": { + "info": { + "version": "1.0", + "title": { + "Ref": "AWS::StackName" + } + }, + "paths": { + "/not-cached-lambda-request": { + "get": { + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "type": "aws_proxy", + "uri": { + "Fn::Sub": "arn:aws-us-gov:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${MyFn.Arn}/invocations" + } + }, + "security": [ + { + "MyLambdaRequestAuth": [] + } + ], + "responses": {} + } + } + }, + "swagger": "2.0", + "securityDefinitions": { + "MyLambdaRequestAuth": { + "in": "header", + "type": "apiKey", + "name": "Unused", + "x-amazon-apigateway-authorizer": { + "type": "request", + "authorizerUri": { + "Fn::Sub": [ + "arn:aws-us-gov:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${__FunctionArn__}/invocations", + { + "__FunctionArn__": { + "Fn::GetAtt": [ + "MyAuthFn", + "Arn" + ] + } + } + ] + }, + "authorizerResultTtlInSeconds": 0 + }, + "x-amazon-apigateway-authtype": "custom" + } + } + }, + "EndpointConfiguration": { + "Types": [ + "REGIONAL" + ] + }, + "Parameters": { + "endpointConfigurationTypes": "REGIONAL" + } + } } } } From 777fcfc7c497329b1ca7f7d653a2dff998ba3ecb Mon Sep 17 00:00:00 2001 From: Mohamed Elasmar <71043312+moelasmar@users.noreply.github.com> Date: Tue, 6 Apr 2021 17:11:12 -0700 Subject: [PATCH 07/16] fix the request parameter parsing, the value can contain dots (#1975) * fix the request parameter parsing, the value can contain dots * fix the unit test for python 2.7 * use built in split, instead of concatenating the string --- samtranslator/swagger/swagger.py | 3 ++- .../input/function_with_request_parameters.yaml | 1 + .../aws-cn/function_with_request_parameters.json | 12 +++++++++--- .../function_with_request_parameters.json | 12 +++++++++--- .../output/function_with_request_parameters.json | 14 ++++++++++---- 5 files changed, 31 insertions(+), 11 deletions(-) diff --git a/samtranslator/swagger/swagger.py b/samtranslator/swagger/swagger.py index 85b131855..64e6699b8 100644 --- a/samtranslator/swagger/swagger.py +++ b/samtranslator/swagger/swagger.py @@ -1134,7 +1134,8 @@ def add_request_parameters_to_method(self, path, method_name, request_parameters parameter_name = request_parameter["Name"] location_name = parameter_name.replace("method.request.", "") - location, name = location_name.split(".") + + location, name = location_name.split(".", 1) if location == "querystring": location = "query" diff --git a/tests/translator/input/function_with_request_parameters.yaml b/tests/translator/input/function_with_request_parameters.yaml index e77a7c4a8..2875457c5 100644 --- a/tests/translator/input/function_with_request_parameters.yaml +++ b/tests/translator/input/function_with_request_parameters.yaml @@ -38,3 +38,4 @@ Resources: RequestParameters: - method.request.querystring.type - method.request.path.id + - method.request.querystring.full.type diff --git a/tests/translator/output/aws-cn/function_with_request_parameters.json b/tests/translator/output/aws-cn/function_with_request_parameters.json index 9cef0cf7d..d5715a354 100644 --- a/tests/translator/output/aws-cn/function_with_request_parameters.json +++ b/tests/translator/output/aws-cn/function_with_request_parameters.json @@ -53,13 +53,13 @@ } } }, - "ServerlessRestApiDeploymentc2741b5220": { + "ServerlessRestApiDeployment32042a0513": { "Type": "AWS::ApiGateway::Deployment", "Properties": { "RestApiId": { "Ref": "ServerlessRestApi" }, - "Description": "RestApi deployment id: c2741b5220c940a753e3d1e18da6763aaba1c19b", + "Description": "RestApi deployment id: 32042a0513cd1c4e5c14794b306c4de10ca5c4af", "StageName": "Stage" } }, @@ -118,7 +118,7 @@ "Type": "AWS::ApiGateway::Stage", "Properties": { "DeploymentId": { - "Ref": "ServerlessRestApiDeploymentc2741b5220" + "Ref": "ServerlessRestApiDeployment32042a0513" }, "RestApiId": { "Ref": "ServerlessRestApi" @@ -247,6 +247,12 @@ "type": "string", "name": "id", "in": "path" + }, + { + "required": false, + "type": "string", + "name": "full.type", + "in": "query" } ] } diff --git a/tests/translator/output/aws-us-gov/function_with_request_parameters.json b/tests/translator/output/aws-us-gov/function_with_request_parameters.json index 44ff1d8f5..67345e559 100644 --- a/tests/translator/output/aws-us-gov/function_with_request_parameters.json +++ b/tests/translator/output/aws-us-gov/function_with_request_parameters.json @@ -53,13 +53,13 @@ } } }, - "ServerlessRestApiDeployment7c706bcd56": { + "ServerlessRestApiDeploymentbe3a929cf9": { "Type": "AWS::ApiGateway::Deployment", "Properties": { "RestApiId": { "Ref": "ServerlessRestApi" }, - "Description": "RestApi deployment id: 7c706bcd56e685afb5882e0219515c9413bcd13b", + "Description": "RestApi deployment id: be3a929cf90555789f2865fc4a96eb9a11ff7a81", "StageName": "Stage" } }, @@ -128,7 +128,7 @@ "Type": "AWS::ApiGateway::Stage", "Properties": { "DeploymentId": { - "Ref": "ServerlessRestApiDeployment7c706bcd56" + "Ref": "ServerlessRestApiDeploymentbe3a929cf9" }, "RestApiId": { "Ref": "ServerlessRestApi" @@ -247,6 +247,12 @@ "type": "string", "name": "id", "in": "path" + }, + { + "required": false, + "type": "string", + "name": "full.type", + "in": "query" } ] } diff --git a/tests/translator/output/function_with_request_parameters.json b/tests/translator/output/function_with_request_parameters.json index 51dcff6ba..940a79710 100644 --- a/tests/translator/output/function_with_request_parameters.json +++ b/tests/translator/output/function_with_request_parameters.json @@ -53,13 +53,13 @@ } } }, - "ServerlessRestApiDeployment2223b43914": { + "ServerlessRestApiDeployment104b236830": { "Type": "AWS::ApiGateway::Deployment", "Properties": { "RestApiId": { "Ref": "ServerlessRestApi" }, - "Description": "RestApi deployment id: 2223b439142974b7a3aad1381ddd39027077ce52", + "Description": "RestApi deployment id: 104b236830d26d2515909073d13fa9c58ad6db49", "StageName": "Stage" } }, @@ -118,7 +118,7 @@ "Type": "AWS::ApiGateway::Stage", "Properties": { "DeploymentId": { - "Ref": "ServerlessRestApiDeployment2223b43914" + "Ref": "ServerlessRestApiDeployment104b236830" }, "RestApiId": { "Ref": "ServerlessRestApi" @@ -239,6 +239,12 @@ "type": "string", "name": "id", "in": "path" + }, + { + "required": false, + "type": "string", + "name": "full.type", + "in": "query" } ] } @@ -272,4 +278,4 @@ } } } -} +} \ No newline at end of file From 944e255c4c3ac435fdb443d7d9b7929d795bb787 Mon Sep 17 00:00:00 2001 From: Mehmet Nuri Deveci <5735811+mndeveci@users.noreply.github.com> Date: Wed, 7 Apr 2021 10:54:04 -0700 Subject: [PATCH 08/16] refactor: Optimize shared API usage plan handling (#1973) * fix: use instance variables for generating shared api usage plan * add extra log statements * fix: Added SAR Support Check (#1972) * Added SAR Support Check * Added docstring and Removed Instance Initialization for Class Method * set log level explicitly * update pyyaml version to get the security update (#1974) * fix: use instance variables for generating shared api usage plan * add extra log statements * set log level explicitly * black formatting * black formatting Co-authored-by: Cosh_ Co-authored-by: Mohamed Elasmar <71043312+moelasmar@users.noreply.github.com> --- samtranslator/model/api/api_generator.py | 44 ++++++++++++++++-------- samtranslator/model/sam_resources.py | 2 ++ samtranslator/translator/translator.py | 3 ++ 3 files changed, 35 insertions(+), 14 deletions(-) diff --git a/samtranslator/model/api/api_generator.py b/samtranslator/model/api/api_generator.py index 4ae7c9df6..b07ae00cc 100644 --- a/samtranslator/model/api/api_generator.py +++ b/samtranslator/model/api/api_generator.py @@ -1,3 +1,4 @@ +import logging from collections import namedtuple from six import string_types from samtranslator.model.intrinsics import ref, fnGetAtt @@ -24,6 +25,9 @@ from samtranslator.translator.arn_generator import ArnGenerator from samtranslator.model.tags.resource_tagging import get_tag_list +LOG = logging.getLogger(__name__) +LOG.setLevel(logging.INFO) + _CORS_WILDCARD = "'*'" CorsProperties = namedtuple( "_CorsProperties", ["AllowMethods", "AllowHeaders", "AllowOrigin", "MaxAge", "AllowCredentials"] @@ -52,12 +56,20 @@ GatewayResponseProperties = ["ResponseParameters", "ResponseTemplates", "StatusCode"] -class ApiGenerator(object): - usage_plan_shared = False - stage_keys_shared = list() - api_stages_shared = list() - depends_on_shared = list() +class SharedApiUsagePlan(object): + """ + Collects API information from different API resources in the same template, + so that these information can be used in the shared usage plan + """ + + def __init__(self): + self.usage_plan_shared = False + self.stage_keys_shared = list() + self.api_stages_shared = list() + self.depends_on_shared = list() + +class ApiGenerator(object): def __init__( self, logical_id, @@ -69,6 +81,7 @@ def __init__( definition_uri, name, stage_name, + shared_api_usage_plan, tags=None, endpoint_configuration=None, method_settings=None, @@ -134,6 +147,7 @@ def __init__( self.models = models self.domain = domain self.description = description + self.shared_api_usage_plan = shared_api_usage_plan def _construct_rest_api(self): """Constructs and returns the ApiGateway RestApi. @@ -630,18 +644,19 @@ def _construct_usage_plan(self, rest_api_stage=None): # create a usage plan for all the Apis elif create_usage_plan == "SHARED": + LOG.info("Creating SHARED usage plan for all the Apis") usage_plan_logical_id = "ServerlessUsagePlan" - if self.logical_id not in ApiGenerator.depends_on_shared: - ApiGenerator.depends_on_shared.append(self.logical_id) + if self.logical_id not in self.shared_api_usage_plan.depends_on_shared: + self.shared_api_usage_plan.depends_on_shared.append(self.logical_id) usage_plan = ApiGatewayUsagePlan( - logical_id=usage_plan_logical_id, depends_on=ApiGenerator.depends_on_shared + logical_id=usage_plan_logical_id, depends_on=self.shared_api_usage_plan.depends_on_shared ) api_stage = dict() api_stage["ApiId"] = ref(self.logical_id) api_stage["Stage"] = ref(rest_api_stage.logical_id) - if api_stage not in ApiGenerator.api_stages_shared: - ApiGenerator.api_stages_shared.append(api_stage) - usage_plan.ApiStages = ApiGenerator.api_stages_shared + if api_stage not in self.shared_api_usage_plan.api_stages_shared: + self.shared_api_usage_plan.api_stages_shared.append(api_stage) + usage_plan.ApiStages = self.shared_api_usage_plan.api_stages_shared api_key = self._construct_api_key(usage_plan_logical_id, create_usage_plan, rest_api_stage) usage_plan_key = self._construct_usage_plan_key(usage_plan_logical_id, create_usage_plan, api_key) @@ -667,15 +682,16 @@ def _construct_api_key(self, usage_plan_logical_id, create_usage_plan, rest_api_ """ if create_usage_plan == "SHARED": # create an api key resource for all the apis + LOG.info("Creating api key resource for all the Apis from SHARED usage plan") api_key_logical_id = "ServerlessApiKey" api_key = ApiGatewayApiKey(logical_id=api_key_logical_id, depends_on=[usage_plan_logical_id]) api_key.Enabled = True stage_key = dict() stage_key["RestApiId"] = ref(self.logical_id) stage_key["StageName"] = ref(rest_api_stage.logical_id) - if stage_key not in ApiGenerator.stage_keys_shared: - ApiGenerator.stage_keys_shared.append(stage_key) - api_key.StageKeys = ApiGenerator.stage_keys_shared + if stage_key not in self.shared_api_usage_plan.stage_keys_shared: + self.shared_api_usage_plan.stage_keys_shared.append(stage_key) + api_key.StageKeys = self.shared_api_usage_plan.stage_keys_shared # for create_usage_plan = "PER_API" else: # create an api key resource for this api diff --git a/samtranslator/model/sam_resources.py b/samtranslator/model/sam_resources.py index a3a9a6d17..66823b93d 100644 --- a/samtranslator/model/sam_resources.py +++ b/samtranslator/model/sam_resources.py @@ -857,6 +857,7 @@ def to_cloudformation(self, **kwargs): self.Domain = intrinsics_resolver.resolve_parameter_refs(self.Domain) self.Auth = intrinsics_resolver.resolve_parameter_refs(self.Auth) redeploy_restapi_parameters = kwargs.get("redeploy_restapi_parameters") + shared_api_usage_plan = kwargs.get("shared_api_usage_plan") api_generator = ApiGenerator( self.logical_id, @@ -868,6 +869,7 @@ def to_cloudformation(self, **kwargs): self.DefinitionUri, self.Name, self.StageName, + shared_api_usage_plan, tags=self.Tags, endpoint_configuration=self.EndpointConfiguration, method_settings=self.MethodSettings, diff --git a/samtranslator/translator/translator.py b/samtranslator/translator/translator.py index d4ca78068..b7449e02e 100644 --- a/samtranslator/translator/translator.py +++ b/samtranslator/translator/translator.py @@ -6,6 +6,7 @@ FeatureToggleDefaultConfigProvider, ) from samtranslator.model import ResourceTypeResolver, sam_resources +from samtranslator.model.api.api_generator import SharedApiUsagePlan from samtranslator.translator.verify_logical_id import verify_unique_logical_id from samtranslator.model.preferences.deployment_preference_collection import DeploymentPreferenceCollection from samtranslator.model.exceptions import ( @@ -111,6 +112,7 @@ def translate(self, sam_template, parameter_values, feature_toggle=None): ) deployment_preference_collection = DeploymentPreferenceCollection() supported_resource_refs = SupportedResourceReferences() + shared_api_usage_plan = SharedApiUsagePlan() document_errors = [] changed_logical_ids = {} for logical_id, resource_dict in self._get_resources_to_iterate(sam_template, macro_resolver): @@ -130,6 +132,7 @@ def translate(self, sam_template, parameter_values, feature_toggle=None): resource_dict, intrinsics_resolver ) kwargs["redeploy_restapi_parameters"] = self.redeploy_restapi_parameters + kwargs["shared_api_usage_plan"] = shared_api_usage_plan translated = macro.to_cloudformation(**kwargs) supported_resource_refs = macro.get_resource_references(translated, supported_resource_refs) From 22949cf96fbcc0707f291aa1aa68f3c534a1e866 Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 12 Apr 2021 18:28:23 +0200 Subject: [PATCH 09/16] Documentation: fix incorrect header (#1979) Fixed the incorrectly formatted header for HTTP API section --- docs/internals/generated_resources.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/internals/generated_resources.rst b/docs/internals/generated_resources.rst index 8b2760d61..ae35e2249 100644 --- a/docs/internals/generated_resources.rst +++ b/docs/internals/generated_resources.rst @@ -129,7 +129,7 @@ AWS::Lambda::Permission MyFunction\ **ThumbnailApi**\ Permission\ **P NOTE: ``ServerlessRestApi*`` resources are generated one per stack. HTTP API -^^^ +^^^^ This is called an "Implicit HTTP API". There can be many functions in the template that define these APIs. Behind the scenes, SAM will collect all implicit HTTP APIs from all Functions in the template, generate an OpenApi doc, and create an implicit ``AWS::Serverless::HttpApi`` using this OpenApi. This API defaults to a StageName called "$default" that cannot be From a291ed4fc09110e6e039088134641b3639f08b89 Mon Sep 17 00:00:00 2001 From: Mehmet Nuri Deveci <5735811+mndeveci@users.noreply.github.com> Date: Thu, 22 Apr 2021 14:16:42 -0700 Subject: [PATCH 10/16] fix: mutable default values in method definitions (#1997) --- samtranslator/intrinsics/resolver.py | 4 +++- samtranslator/model/apigateway.py | 4 +++- samtranslator/model/intrinsics.py | 4 +++- samtranslator/model/stepfunctions/generators.py | 4 +++- samtranslator/open_api/open_api.py | 4 +++- .../plugins/application/serverless_app_plugin.py | 4 +++- samtranslator/swagger/swagger.py | 4 +++- samtranslator/translator/arn_generator.py | 6 +++--- samtranslator/translator/translator.py | 7 +++++-- tests/intrinsics/test_resolver.py | 5 ----- tests/translator/test_arn_generator.py | 11 ++++------- 11 files changed, 33 insertions(+), 24 deletions(-) diff --git a/samtranslator/intrinsics/resolver.py b/samtranslator/intrinsics/resolver.py index 519f856fe..af79704a9 100644 --- a/samtranslator/intrinsics/resolver.py +++ b/samtranslator/intrinsics/resolver.py @@ -8,7 +8,7 @@ class IntrinsicsResolver(object): - def __init__(self, parameters, supported_intrinsics=DEFAULT_SUPPORTED_INTRINSICS): + def __init__(self, parameters, supported_intrinsics=None): """ Instantiate the resolver :param dict parameters: Map of parameter names to their values @@ -17,6 +17,8 @@ def __init__(self, parameters, supported_intrinsics=DEFAULT_SUPPORTED_INTRINSICS :raises TypeError: If parameters or the supported_intrinsics arguments are invalid """ + if supported_intrinsics is None: + supported_intrinsics = DEFAULT_SUPPORTED_INTRINSICS if parameters is None or not isinstance(parameters, dict): raise InvalidDocumentException( [InvalidTemplateException("'Mappings' or 'Parameters' is either null or not a valid dictionary.")] diff --git a/samtranslator/model/apigateway.py b/samtranslator/model/apigateway.py index 607c38529..0e768918f 100644 --- a/samtranslator/model/apigateway.py +++ b/samtranslator/model/apigateway.py @@ -231,8 +231,10 @@ def __init__( function_payload_type=None, function_invoke_role=None, is_aws_iam_authorizer=False, - authorization_scopes=[], + authorization_scopes=None, ): + if authorization_scopes is None: + authorization_scopes = [] if function_payload_type not in ApiGatewayAuthorizer._VALID_FUNCTION_PAYLOAD_TYPES: raise InvalidResourceException( api_logical_id, diff --git a/samtranslator/model/intrinsics.py b/samtranslator/model/intrinsics.py index c81e40d00..e95a5d6d0 100644 --- a/samtranslator/model/intrinsics.py +++ b/samtranslator/model/intrinsics.py @@ -24,7 +24,9 @@ def fnAnd(argument_list): return {"Fn::And": argument_list} -def make_conditional(condition, true_data, false_data={"Ref": "AWS::NoValue"}): +def make_conditional(condition, true_data, false_data=None): + if false_data is None: + false_data = {"Ref": "AWS::NoValue"} return {"Fn::If": [condition, true_data, false_data]} diff --git a/samtranslator/model/stepfunctions/generators.py b/samtranslator/model/stepfunctions/generators.py index 8e1797c7d..f8431c189 100644 --- a/samtranslator/model/stepfunctions/generators.py +++ b/samtranslator/model/stepfunctions/generators.py @@ -286,7 +286,7 @@ def _replace_dynamic_values_with_substitutions(self, input): location[path[-1]] = sub_key return substitution_map - def _get_paths_to_intrinsics(self, input, path=[]): + def _get_paths_to_intrinsics(self, input, path=None): """ Returns all paths to dynamic values within a dictionary @@ -294,6 +294,8 @@ def _get_paths_to_intrinsics(self, input, path=[]): :param path: Optional list to keep track of the path to the input dictionary :returns list: List of keys that defines the path to a dynamic value within the input dictionary """ + if path is None: + path = [] dynamic_value_paths = [] if isinstance(input, dict): iterator = input.items() diff --git a/samtranslator/open_api/open_api.py b/samtranslator/open_api/open_api.py index fc0f77a26..af6b47e49 100644 --- a/samtranslator/open_api/open_api.py +++ b/samtranslator/open_api/open_api.py @@ -391,7 +391,7 @@ def add_auth_to_method(self, path, method_name, auth, api): if method_authorizer: self._set_method_authorizer(path, method_name, method_authorizer, authorizers, authorization_scopes) - def _set_method_authorizer(self, path, method_name, authorizer_name, authorizers, authorization_scopes=[]): + def _set_method_authorizer(self, path, method_name, authorizer_name, authorizers, authorization_scopes=None): """ Adds the authorizer_name to the security block for each method on this path. This is used to configure the authorizer for individual functions. @@ -402,6 +402,8 @@ def _set_method_authorizer(self, path, method_name, authorizer_name, authorizers authorizers param. :param list authorization_scopes: list of strings that are the auth scopes for this method """ + if authorization_scopes is None: + authorization_scopes = [] normalized_method_name = self._normalize_method_name(method_name) # It is possible that the method could have two definitions in a Fn::If block. for method_definition in self.get_method_contents(self.get_path(path)[normalized_method_name]): diff --git a/samtranslator/plugins/application/serverless_app_plugin.py b/samtranslator/plugins/application/serverless_app_plugin.py index 02333e0f4..05741c5df 100644 --- a/samtranslator/plugins/application/serverless_app_plugin.py +++ b/samtranslator/plugins/application/serverless_app_plugin.py @@ -42,7 +42,7 @@ class ServerlessAppPlugin(BasePlugin): LOCATION_KEY = "Location" TEMPLATE_URL_KEY = "TemplateUrl" - def __init__(self, sar_client=None, wait_for_template_active_status=False, validate_only=False, parameters={}): + def __init__(self, sar_client=None, wait_for_template_active_status=False, validate_only=False, parameters=None): """ Initialize the plugin. @@ -52,6 +52,8 @@ def __init__(self, sar_client=None, wait_for_template_active_status=False, valid :param bool validate_only: Flag to only validate application access (uses get_application API instead) """ super(ServerlessAppPlugin, self).__init__(ServerlessAppPlugin.__name__) + if parameters is None: + parameters = {} self._applications = {} self._in_progress_templates = [] self._sar_client = sar_client diff --git a/samtranslator/swagger/swagger.py b/samtranslator/swagger/swagger.py index 64e6699b8..4345373c0 100644 --- a/samtranslator/swagger/swagger.py +++ b/samtranslator/swagger/swagger.py @@ -698,7 +698,7 @@ def add_auth_to_method(self, path, method_name, auth, api): if method_apikey_required is not None: self._set_method_apikey_handling(path, method_name, method_apikey_required) - def _set_method_authorizer(self, path, method_name, authorizer_name, authorizers={}, method_scopes=None): + def _set_method_authorizer(self, path, method_name, authorizer_name, authorizers=None, method_scopes=None): """ Adds the authorizer_name to the security block for each method on this path. This is used to configure the authorizer for individual functions. @@ -708,6 +708,8 @@ def _set_method_authorizer(self, path, method_name, authorizer_name, authorizers :param string authorizer_name: Name of the authorizer to use. Must be a key in the authorizers param. """ + if authorizers is None: + authorizers = {} normalized_method_name = self._normalize_method_name(method_name) # It is possible that the method could have two definitions in a Fn::If block. for method_definition in self.get_method_contents(self.get_path(path)[normalized_method_name]): diff --git a/samtranslator/translator/arn_generator.py b/samtranslator/translator/arn_generator.py index 9394a6c0a..897661e8d 100644 --- a/samtranslator/translator/arn_generator.py +++ b/samtranslator/translator/arn_generator.py @@ -6,7 +6,7 @@ class NoRegionFound(Exception): class ArnGenerator(object): - class_boto_session = None + BOTO_SESSION_REGION_NAME = None @classmethod def generate_arn(cls, partition, service, resource, include_account_id=True): @@ -50,10 +50,10 @@ def get_partition_name(cls, region=None): # Use Boto3 to get the region where code is running. This uses Boto's regular region resolution # mechanism, starting from AWS_DEFAULT_REGION environment variable. - if ArnGenerator.class_boto_session is None: + if ArnGenerator.BOTO_SESSION_REGION_NAME is None: region = boto3.session.Session().region_name else: - region = ArnGenerator.class_boto_session.region_name + region = ArnGenerator.BOTO_SESSION_REGION_NAME # If region is still None, then we could not find the region. This will only happen # in the local context. When this is deployed, we will be able to find the region like diff --git a/samtranslator/translator/translator.py b/samtranslator/translator/translator.py index b7449e02e..c7d7c415b 100644 --- a/samtranslator/translator/translator.py +++ b/samtranslator/translator/translator.py @@ -45,7 +45,8 @@ def __init__(self, managed_policy_map, sam_parser, plugins=None, boto_session=No self.feature_toggle = None self.boto_session = boto_session - ArnGenerator.class_boto_session = self.boto_session + if self.boto_session: + ArnGenerator.BOTO_SESSION_REGION_NAME = self.boto_session.region_name def _get_function_names(self, resource_dict, intrinsics_resolver): """ @@ -226,7 +227,7 @@ def _get_resources_to_iterate(self, sam_template, macro_resolver): return functions + statemachines + apis + others -def prepare_plugins(plugins, parameters={}): +def prepare_plugins(plugins, parameters=None): """ Creates & returns a plugins object with the given list of plugins installed. In addition to the given plugins, we will also install a few "required" plugins that are necessary to provide complete support for SAM template spec. @@ -236,6 +237,8 @@ def prepare_plugins(plugins, parameters={}): :return samtranslator.plugins.SamPlugins: Instance of `SamPlugins` """ + if parameters is None: + parameters = {} required_plugins = [ DefaultDefinitionBodyPlugin(), make_implicit_rest_api_plugin(), diff --git a/tests/intrinsics/test_resolver.py b/tests/intrinsics/test_resolver.py index 2f62b510f..946307b87 100644 --- a/tests/intrinsics/test_resolver.py +++ b/tests/intrinsics/test_resolver.py @@ -192,11 +192,6 @@ class SomeAction(Action): with self.assertRaises(TypeError): IntrinsicsResolver({}, supported_intrinsics) - def test_configure_supported_intrinsics_must_error_for_none_input(self): - - with self.assertRaises(TypeError): - IntrinsicsResolver({}, None) - def test_configure_supported_intrinsics_must_error_for_non_dict_input(self): with self.assertRaises(TypeError): diff --git a/tests/translator/test_arn_generator.py b/tests/translator/test_arn_generator.py index 461840cdf..200f90414 100644 --- a/tests/translator/test_arn_generator.py +++ b/tests/translator/test_arn_generator.py @@ -1,13 +1,13 @@ from unittest import TestCase from parameterized import parameterized -from mock import Mock, patch +from mock import patch from samtranslator.translator.arn_generator import ArnGenerator, NoRegionFound class TestArnGenerator(TestCase): def setUp(self): - ArnGenerator.class_boto_session = None + ArnGenerator.BOTO_SESSION_REGION_NAME = None @parameterized.expand( [("us-east-1", "aws"), ("cn-east-1", "aws-cn"), ("us-gov-west-1", "aws-us-gov"), ("US-EAST-1", "aws")] @@ -23,13 +23,10 @@ def test_get_partition_name_raise_NoRegionFound(self): ArnGenerator.get_partition_name(None) def test_get_partition_name_from_boto_session(self): - boto_session_mock = Mock() - boto_session_mock.region_name = "us-east-1" - - ArnGenerator.class_boto_session = boto_session_mock + ArnGenerator.BOTO_SESSION_REGION_NAME = "us-east-1" actual = ArnGenerator.get_partition_name() self.assertEqual(actual, "aws") - ArnGenerator.class_boto_session = None + ArnGenerator.BOTO_SESSION_REGION_NAME = None From a1fe09bdfb59d5651dc74b2895f2433bf7c6c792 Mon Sep 17 00:00:00 2001 From: Mehmet Nuri Deveci <5735811+mndeveci@users.noreply.github.com> Date: Thu, 22 Apr 2021 14:23:41 -0700 Subject: [PATCH 11/16] fix: remove explicit logging level set in single module (#1998) --- samtranslator/model/api/api_generator.py | 1 - 1 file changed, 1 deletion(-) diff --git a/samtranslator/model/api/api_generator.py b/samtranslator/model/api/api_generator.py index b07ae00cc..5606002fe 100644 --- a/samtranslator/model/api/api_generator.py +++ b/samtranslator/model/api/api_generator.py @@ -26,7 +26,6 @@ from samtranslator.model.tags.resource_tagging import get_tag_list LOG = logging.getLogger(__name__) -LOG.setLevel(logging.INFO) _CORS_WILDCARD = "'*'" CorsProperties = namedtuple( From 6cc9add567219f8fc3149448666e0a51fbfbd2f4 Mon Sep 17 00:00:00 2001 From: Jacob Fuss <32497805+jfuss@users.noreply.github.com> Date: Wed, 5 May 2021 14:59:52 -0500 Subject: [PATCH 12/16] fix: Crash when using an invalid method in open api (#2001) When customers use auth and define an invalid method in the open api definition, SAM would return a 'server error'. This was actually due to SAM attempting to get the method from the path. If the method was not a supported method and non-lowercase, SAM would attempt to fetch the lower case method and crash with a KeyError. This PR addresses that by checking for the valid methods supported. Co-authored-by: Jacob Fuss --- samtranslator/swagger/swagger.py | 13 +++++ tests/swagger/test_swagger.py | 36 ++++++++++++++ .../error_api_with_invalid_path_object.yaml | 47 +++++++++++++++++++ .../error_invalid_method_definition.yaml | 2 +- .../error_api_with_invalid_path_object.json | 3 ++ tests/translator/test_translator.py | 1 - 6 files changed, 100 insertions(+), 2 deletions(-) create mode 100644 tests/translator/input/error_api_with_invalid_path_object.yaml create mode 100644 tests/translator/output/error_api_with_invalid_path_object.json diff --git a/samtranslator/swagger/swagger.py b/samtranslator/swagger/swagger.py index 4345373c0..d6aa36c2f 100644 --- a/samtranslator/swagger/swagger.py +++ b/samtranslator/swagger/swagger.py @@ -531,6 +531,19 @@ def set_path_default_authorizer( if add_default_auth_to_preflight or normalized_method_name != "options": normalized_method_name = self._normalize_method_name(method_name) # It is possible that the method could have two definitions in a Fn::If block. + + # check for valid methods + if normalized_method_name.upper() not in self._ALL_HTTP_METHODS: + raise InvalidDocumentException( + [ + InvalidTemplateException( + "Path '{}' contains method '{}' which is not a supported method {}".format( + path, method_name, self._ALL_HTTP_METHODS + ) + ) + ] + ) + for method_definition in self.get_method_contents(self.get_path(path)[normalized_method_name]): # If no integration given, then we don't need to process this definition (could be AWS::NoValue) diff --git a/tests/swagger/test_swagger.py b/tests/swagger/test_swagger.py index cf6147726..fa3f8e580 100644 --- a/tests/swagger/test_swagger.py +++ b/tests/swagger/test_swagger.py @@ -1456,3 +1456,39 @@ def test_should_include_none_if_default_is_overwritte(self): self.editor.add_auth_to_method("/cognito", "get", auth, self.api) self.assertEqual([{"NONE": []}], self.editor.swagger["paths"]["/cognito"]["get"]["security"]) + + +class TestSwaggerEditor_set_path_default_authorizer(TestCase): + def setUp(self): + self.api = api = { + "Auth": { + "Authorizers": {"MyOtherCognitoAuth": {}, "MyCognitoAuth": {}}, + "DefaultAuthorizer": "MyCognitoAuth", + } + } + self.editor = SwaggerEditor( + { + "swagger": "2.0", + "paths": { + "/cognito": { + "nonMethod": { + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "type": "aws_proxy", + "uri": { + "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${MyFn.Arn}/invocations" + }, + }, + "security": [], + "responses": {}, + } + } + }, + } + ) + + def test_should_fail_when_path_methods_are_invalid(self): + with self.assertRaises(InvalidDocumentException): + self.editor.set_path_default_authorizer( + "/cognito", "MyCognitoAuth", {"MyOtherCognitoAuth": {}, "MyCognitoAuth": {}} + ) diff --git a/tests/translator/input/error_api_with_invalid_path_object.yaml b/tests/translator/input/error_api_with_invalid_path_object.yaml new file mode 100644 index 000000000..3b51d2dc5 --- /dev/null +++ b/tests/translator/input/error_api_with_invalid_path_object.yaml @@ -0,0 +1,47 @@ +Globals: + Api: + Name: "some api" + Variables: + SomeVar: Value + Auth: + DefaultAuthorizer: MyCognitoAuth + Authorizers: + MyCognitoAuth: + UserPoolArn: !GetAtt MyUserPool.Arn + +Resources: + ImplicitApiFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://sam-demo-bucket/member_portal.zip + Handler: index.gethtml + Runtime: nodejs12.x + + ExplicitApi: + Type: AWS::Serverless::Api + Properties: + StageName: SomeStage + DefinitionBody: + swagger: 2.0 + paths: + "/a": + SomeInvalidKey: + x-amazon-apigateway-integration: + httpMethod: POST + type: aws_proxy + uri: !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${ImplicitApiFunction.Arn}/invocations + responses: {} + + MyUserPool: + Type: AWS::Cognito::UserPool + Properties: + UserPoolName: UserPoolName + Policies: + PasswordPolicy: + MinimumLength: 8 + UsernameAttributes: + - email + Schema: + - AttributeDataType: String + Name: email + Required: false \ No newline at end of file diff --git a/tests/translator/input/error_invalid_method_definition.yaml b/tests/translator/input/error_invalid_method_definition.yaml index fd9b6617f..440ffca7d 100644 --- a/tests/translator/input/error_invalid_method_definition.yaml +++ b/tests/translator/input/error_invalid_method_definition.yaml @@ -42,7 +42,7 @@ Resources: description: Application domain type: string required: true - tags: + options: - InvalidMethodDefinition get: x-amazon-apigateway-integration: diff --git a/tests/translator/output/error_api_with_invalid_path_object.json b/tests/translator/output/error_api_with_invalid_path_object.json new file mode 100644 index 000000000..03e125d85 --- /dev/null +++ b/tests/translator/output/error_api_with_invalid_path_object.json @@ -0,0 +1,3 @@ +{ + "errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 1. Path '/a' contains method 'SomeInvalidKey' which is not a supported method ['OPTIONS', 'GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'PATCH']" +} \ No newline at end of file diff --git a/tests/translator/test_translator.py b/tests/translator/test_translator.py index 05948913f..27667adeb 100644 --- a/tests/translator/test_translator.py +++ b/tests/translator/test_translator.py @@ -413,7 +413,6 @@ def test_transform_success(self, testcase, partition_with_region): ], # Run all the above tests against each of the list of partitions to test against ) ) - @pytest.mark.slow @patch( "samtranslator.plugins.application.serverless_app_plugin.ServerlessAppPlugin._sar_service_call", mock_sar_service_call, From 2fed3a895d8ea6844d11b206ad80f31346002cf6 Mon Sep 17 00:00:00 2001 From: Qingchuan Ma <69653965+qingchm@users.noreply.github.com> Date: Sat, 8 May 2021 12:55:34 -0700 Subject: [PATCH 13/16] feat: Resource level attributes support (#2008) * Fix for invalid MQ event source managed policy * Fix for invalid managed policy for MQ, included support for new MQ event source property, updated test cases * Black reformatting * Test case changes * Changed policy name * Modified test cases with new policy name * Added resource attributes and unit tests * Resource attributes initial work * Passthrough attributes for some resources, updated some tests * Resolve merge conflicts * Fixed a typo * Modified implicit api plugin for resource attributes support * Partial update of the tests * Partially updated test cases, black reformatted * Partially updated test templates * Partially updated test templates * Partially updated test templates * Added event bridge support for passthrough resource attributes * Partially updated test templates (up to function with amq kms) * Partially updated test templates (up to sns) * Partially updated test templates (all the ones left) * Prevented passthrough resource attributes from changing layer version hashes * Added test to verify resource passthrough precedence for implicit api * Modified tests related to lambda layer to revert the hash changes, keeping the hash the same with resource attributes added * fix: mutable default values in method definitions (#1997) * fix: remove explicit logging level set in single module (#1998) * run automated tests for resource level attribute support * Skipping metadata in layer hashing * Refactored the classes for TestTranslatorEndToEnd and TestResourceLevelAttributes to share the same parent class * Added new translator tests for version and layer resources * Added new unit tests * Removed after transform resource plugin * Black reformatting * Refactoring implicit api plugin support for DeletionPolicy and UpdateReplacePolicy * Refactoring to improve code quality * Added simple documentation * Black reformatting * Added input template that was missing * Refactoring: use sets instead of lists for implicit api plugin * Changing import to be compatible with py2.7 * Changing test deployment hashes to their actual values Co-authored-by: Mehmet Nuri Deveci <5735811+mndeveci@users.noreply.github.com> --- samtranslator/model/__init__.py | 29 +- samtranslator/model/api/api_generator.py | 30 +- samtranslator/model/eventbridge_utils.py | 10 +- .../model/eventsources/cloudwatchlogs.py | 8 +- samtranslator/model/eventsources/pull.py | 7 +- samtranslator/model/eventsources/push.py | 69 +- samtranslator/model/sam_resources.py | 44 +- samtranslator/model/stepfunctions/events.py | 18 +- .../plugins/api/implicit_api_plugin.py | 83 +- .../plugins/api/implicit_http_api_plugin.py | 16 +- .../plugins/api/implicit_rest_api_plugin.py | 14 +- samtranslator/sdk/resource.py | 16 + .../validator/sam_schema/schema.json | 33 +- tests/model/test_sam_resources.py | 8 + tests/plugins/api/test_implicit_api_plugin.py | 47 +- tests/test_model.py | 20 + .../api_with_swagger_authorizer_none.yaml | 117 +++ ...plicit_api_deletion_policy_precedence.yaml | 32 + .../layer_deletion_policy_precedence.yaml | 18 + .../version_deletion_policy_precedence.yaml | 19 + .../api_with_swagger_authorizer_none.json | 860 ++++++++--------- .../api_with_usageplans_intrinsics.json | 9 +- .../api_with_swagger_authorizer_none.json | 906 +++++++++--------- .../api_with_usageplans_intrinsics.json | 9 +- .../aws-cn/function_event_conditions.json | 1 + ...plicit_api_deletion_policy_precedence.json | 242 +++++ .../layer_deletion_policy_precedence.json | 37 + .../version_deletion_policy_precedence.json | 163 ++++ .../api_with_swagger_authorizer_none.json | 906 +++++++++--------- .../api_with_usageplans_intrinsics.json | 9 +- .../aws-us-gov/function_event_conditions.json | 1 + ...plicit_api_deletion_policy_precedence.json | 242 +++++ .../layer_deletion_policy_precedence.json | 37 + .../version_deletion_policy_precedence.json | 163 ++++ .../output/function_event_conditions.json | 1 + ...plicit_api_deletion_policy_precedence.json | 234 +++++ .../layer_deletion_policy_precedence.json | 37 + .../version_deletion_policy_precedence.json | 163 ++++ .../test_resource_level_attributes.py | 86 ++ tests/translator/test_translator.py | 233 ++--- 40 files changed, 3419 insertions(+), 1558 deletions(-) create mode 100644 tests/translator/input/api_with_swagger_authorizer_none.yaml create mode 100644 tests/translator/input/implicit_api_deletion_policy_precedence.yaml create mode 100644 tests/translator/input/layer_deletion_policy_precedence.yaml create mode 100644 tests/translator/input/version_deletion_policy_precedence.yaml create mode 100644 tests/translator/output/aws-cn/implicit_api_deletion_policy_precedence.json create mode 100644 tests/translator/output/aws-cn/layer_deletion_policy_precedence.json create mode 100644 tests/translator/output/aws-cn/version_deletion_policy_precedence.json create mode 100644 tests/translator/output/aws-us-gov/implicit_api_deletion_policy_precedence.json create mode 100644 tests/translator/output/aws-us-gov/layer_deletion_policy_precedence.json create mode 100644 tests/translator/output/aws-us-gov/version_deletion_policy_precedence.json create mode 100644 tests/translator/output/implicit_api_deletion_policy_precedence.json create mode 100644 tests/translator/output/layer_deletion_policy_precedence.json create mode 100644 tests/translator/output/version_deletion_policy_precedence.json create mode 100644 tests/translator/test_resource_level_attributes.py diff --git a/samtranslator/model/__init__.py b/samtranslator/model/__init__.py index 150645589..d535fb142 100644 --- a/samtranslator/model/__init__.py +++ b/samtranslator/model/__init__.py @@ -43,7 +43,11 @@ class Resource(object): property_types = None _keywords = ["logical_id", "relative_id", "depends_on", "resource_attributes"] - _supported_resource_attributes = ["DeletionPolicy", "UpdatePolicy", "Condition"] + # For attributes in this list, they will be passed into the translated template for the same resource itself. + _supported_resource_attributes = ["DeletionPolicy", "UpdatePolicy", "Condition", "UpdateReplacePolicy", "Metadata"] + # For attributes in this list, they will be passed into the translated template for the same resource, + # as well as all the auto-generated resources that are created from this resource. + _pass_through_attributes = ["Condition", "DeletionPolicy", "UpdateReplacePolicy"] # Runtime attributes that can be qureied resource. They are CloudFormation attributes like ARN, Name etc that # will be resolvable at runtime. This map will be implemented by sub-classes to express list of attributes they @@ -76,6 +80,22 @@ def __init__(self, logical_id, relative_id=None, depends_on=None, attributes=Non for attr, value in attributes.items(): self.set_resource_attribute(attr, value) + @classmethod + def get_supported_resource_attributes(cls): + """ + A getter method for the supported resource attributes + returns: a tuple that contains the name of all supported resource attributes + """ + return tuple(cls._supported_resource_attributes) + + @classmethod + def get_pass_through_attributes(cls): + """ + A getter method for the resource attributes to be passed to auto-generated resources + returns: a tuple that contains the name of all pass through attributes + """ + return tuple(cls._pass_through_attributes) + @classmethod def from_dict(cls, logical_id, resource_dict, relative_id=None, sam_plugins=None): """Constructs a Resource object with the given logical id, based on the given resource dict. The resource dict @@ -318,9 +338,10 @@ def get_passthrough_resource_attributes(self): :return: Dictionary of resource attributes. """ - attributes = None - if "Condition" in self.resource_attributes: - attributes = {"Condition": self.resource_attributes["Condition"]} + attributes = {} + for resource_attribute in self.get_pass_through_attributes(): + if resource_attribute in self.resource_attributes: + attributes[resource_attribute] = self.resource_attributes.get(resource_attribute) return attributes diff --git a/samtranslator/model/api/api_generator.py b/samtranslator/model/api/api_generator.py index 5606002fe..eef1d3d27 100644 --- a/samtranslator/model/api/api_generator.py +++ b/samtranslator/model/api/api_generator.py @@ -630,7 +630,11 @@ def _construct_usage_plan(self, rest_api_stage=None): # create usage plan for this api only elif usage_plan_properties.get("CreateUsagePlan") == "PER_API": usage_plan_logical_id = self.logical_id + "UsagePlan" - usage_plan = ApiGatewayUsagePlan(logical_id=usage_plan_logical_id, depends_on=[self.logical_id]) + usage_plan = ApiGatewayUsagePlan( + logical_id=usage_plan_logical_id, + depends_on=[self.logical_id], + attributes=self.passthrough_resource_attributes, + ) api_stages = list() api_stage = dict() api_stage["ApiId"] = ref(self.logical_id) @@ -648,7 +652,9 @@ def _construct_usage_plan(self, rest_api_stage=None): if self.logical_id not in self.shared_api_usage_plan.depends_on_shared: self.shared_api_usage_plan.depends_on_shared.append(self.logical_id) usage_plan = ApiGatewayUsagePlan( - logical_id=usage_plan_logical_id, depends_on=self.shared_api_usage_plan.depends_on_shared + logical_id=usage_plan_logical_id, + depends_on=self.shared_api_usage_plan.depends_on_shared, + attributes=self.passthrough_resource_attributes, ) api_stage = dict() api_stage["ApiId"] = ref(self.logical_id) @@ -683,7 +689,11 @@ def _construct_api_key(self, usage_plan_logical_id, create_usage_plan, rest_api_ # create an api key resource for all the apis LOG.info("Creating api key resource for all the Apis from SHARED usage plan") api_key_logical_id = "ServerlessApiKey" - api_key = ApiGatewayApiKey(logical_id=api_key_logical_id, depends_on=[usage_plan_logical_id]) + api_key = ApiGatewayApiKey( + logical_id=api_key_logical_id, + depends_on=[usage_plan_logical_id], + attributes=self.passthrough_resource_attributes, + ) api_key.Enabled = True stage_key = dict() stage_key["RestApiId"] = ref(self.logical_id) @@ -695,7 +705,12 @@ def _construct_api_key(self, usage_plan_logical_id, create_usage_plan, rest_api_ else: # create an api key resource for this api api_key_logical_id = self.logical_id + "ApiKey" - api_key = ApiGatewayApiKey(logical_id=api_key_logical_id, depends_on=[usage_plan_logical_id]) + api_key = ApiGatewayApiKey( + logical_id=api_key_logical_id, + depends_on=[usage_plan_logical_id], + attributes=self.passthrough_resource_attributes, + ) + # api_key = ApiGatewayApiKey(logical_id=api_key_logical_id, depends_on=[usage_plan_logical_id]) api_key.Enabled = True stage_keys = list() stage_key = dict() @@ -720,7 +735,12 @@ def _construct_usage_plan_key(self, usage_plan_logical_id, create_usage_plan, ap # create a mapping between api key and the usage plan usage_plan_key_logical_id = self.logical_id + "UsagePlanKey" - usage_plan_key = ApiGatewayUsagePlanKey(logical_id=usage_plan_key_logical_id, depends_on=[api_key.logical_id]) + usage_plan_key = ApiGatewayUsagePlanKey( + logical_id=usage_plan_key_logical_id, + depends_on=[api_key.logical_id], + attributes=self.passthrough_resource_attributes, + ) + # usage_plan_key = ApiGatewayUsagePlanKey(logical_id=usage_plan_key_logical_id, depends_on=[api_key.logical_id]) usage_plan_key.KeyId = ref(api_key.logical_id) usage_plan_key.KeyType = "API_KEY" usage_plan_key.UsagePlanId = ref(usage_plan_logical_id) diff --git a/samtranslator/model/eventbridge_utils.py b/samtranslator/model/eventbridge_utils.py index 39bf40745..bb407cd84 100644 --- a/samtranslator/model/eventbridge_utils.py +++ b/samtranslator/model/eventbridge_utils.py @@ -4,15 +4,15 @@ class EventBridgeRuleUtils: @staticmethod - def create_dead_letter_queue_with_policy(rule_logical_id, rule_arn, queue_logical_id=None): + def create_dead_letter_queue_with_policy(rule_logical_id, rule_arn, queue_logical_id=None, attributes=None): resources = [] - queue = SQSQueue(queue_logical_id or rule_logical_id + "Queue") + queue = SQSQueue(queue_logical_id or rule_logical_id + "Queue", attributes=attributes) dlq_queue_arn = queue.get_runtime_attr("arn") dlq_queue_url = queue.get_runtime_attr("queue_url") # grant necessary permission to Eventbridge Rule resource for sending messages to dead-letter queue - policy = SQSQueuePolicy(rule_logical_id + "QueuePolicy") + policy = SQSQueuePolicy(rule_logical_id + "QueuePolicy", attributes=attributes) policy.PolicyDocument = SQSQueuePolicies.eventbridge_dlq_send_message_resource_based_policy( rule_arn, dlq_queue_arn ) @@ -41,14 +41,14 @@ def validate_dlq_config(source_logical_id, dead_letter_config): raise InvalidEventException(source_logical_id, "No 'Arn' or 'Type' property provided for DeadLetterConfig") @staticmethod - def get_dlq_queue_arn_and_resources(cw_event_source, source_arn): + def get_dlq_queue_arn_and_resources(cw_event_source, source_arn, attributes): """returns dlq queue arn and dlq_resources, assuming cw_event_source.DeadLetterConfig has been validated""" dlq_queue_arn = cw_event_source.DeadLetterConfig.get("Arn") if dlq_queue_arn is not None: return dlq_queue_arn, [] queue_logical_id = cw_event_source.DeadLetterConfig.get("QueueLogicalId") dlq_resources = EventBridgeRuleUtils.create_dead_letter_queue_with_policy( - cw_event_source.logical_id, source_arn, queue_logical_id + cw_event_source.logical_id, source_arn, queue_logical_id, attributes ) dlq_queue_arn = dlq_resources[0].get_runtime_attr("arn") return dlq_queue_arn, dlq_resources diff --git a/samtranslator/model/eventsources/cloudwatchlogs.py b/samtranslator/model/eventsources/cloudwatchlogs.py index 8adc32439..5cce4d0aa 100644 --- a/samtranslator/model/eventsources/cloudwatchlogs.py +++ b/samtranslator/model/eventsources/cloudwatchlogs.py @@ -43,11 +43,13 @@ def get_source_arn(self): ) def get_subscription_filter(self, function, permission): - subscription_filter = SubscriptionFilter(self.logical_id, depends_on=[permission.logical_id]) + subscription_filter = SubscriptionFilter( + self.logical_id, + depends_on=[permission.logical_id], + attributes=function.get_passthrough_resource_attributes(), + ) subscription_filter.LogGroupName = self.LogGroupName subscription_filter.FilterPattern = self.FilterPattern subscription_filter.DestinationArn = function.get_runtime_attr("arn") - if "Condition" in function.resource_attributes: - subscription_filter.set_resource_attribute("Condition", function.resource_attributes["Condition"]) return subscription_filter diff --git a/samtranslator/model/eventsources/pull.py b/samtranslator/model/eventsources/pull.py index 747968bb9..106700d63 100644 --- a/samtranslator/model/eventsources/pull.py +++ b/samtranslator/model/eventsources/pull.py @@ -60,7 +60,9 @@ def to_cloudformation(self, **kwargs): resources = [] - lambda_eventsourcemapping = LambdaEventSourceMapping(self.logical_id) + lambda_eventsourcemapping = LambdaEventSourceMapping( + self.logical_id, attributes=function.get_passthrough_resource_attributes() + ) resources.append(lambda_eventsourcemapping) try: @@ -122,9 +124,6 @@ def to_cloudformation(self, **kwargs): ) lambda_eventsourcemapping.DestinationConfig = self.DestinationConfig - if "Condition" in function.resource_attributes: - lambda_eventsourcemapping.set_resource_attribute("Condition", function.resource_attributes["Condition"]) - if "role" in kwargs: self._link_policy(kwargs["role"], destination_config_policy) diff --git a/samtranslator/model/eventsources/push.py b/samtranslator/model/eventsources/push.py index d134f1940..39792c211 100644 --- a/samtranslator/model/eventsources/push.py +++ b/samtranslator/model/eventsources/push.py @@ -113,7 +113,8 @@ def to_cloudformation(self, **kwargs): resources = [] - events_rule = EventsRule(self.logical_id) + passthrough_resource_attributes = function.get_passthrough_resource_attributes() + events_rule = EventsRule(self.logical_id, attributes=passthrough_resource_attributes) resources.append(events_rule) events_rule.ScheduleExpression = self.Schedule @@ -126,13 +127,13 @@ def to_cloudformation(self, **kwargs): dlq_queue_arn = None if self.DeadLetterConfig is not None: EventBridgeRuleUtils.validate_dlq_config(self.logical_id, self.DeadLetterConfig) - dlq_queue_arn, dlq_resources = EventBridgeRuleUtils.get_dlq_queue_arn_and_resources(self, source_arn) + dlq_queue_arn, dlq_resources = EventBridgeRuleUtils.get_dlq_queue_arn_and_resources( + self, source_arn, passthrough_resource_attributes + ) resources.extend(dlq_resources) events_rule.Targets = [self._construct_target(function, dlq_queue_arn)] - if CONDITION in function.resource_attributes: - events_rule.set_resource_attribute(CONDITION, function.resource_attributes[CONDITION]) resources.append(self._construct_permission(function, source_arn=source_arn)) return resources @@ -186,7 +187,8 @@ def to_cloudformation(self, **kwargs): resources = [] - events_rule = EventsRule(self.logical_id) + passthrough_resource_attributes = function.get_passthrough_resource_attributes() + events_rule = EventsRule(self.logical_id, attributes=passthrough_resource_attributes) events_rule.EventBusName = self.EventBusName events_rule.EventPattern = self.Pattern source_arn = events_rule.get_runtime_attr("arn") @@ -194,12 +196,12 @@ def to_cloudformation(self, **kwargs): dlq_queue_arn = None if self.DeadLetterConfig is not None: EventBridgeRuleUtils.validate_dlq_config(self.logical_id, self.DeadLetterConfig) - dlq_queue_arn, dlq_resources = EventBridgeRuleUtils.get_dlq_queue_arn_and_resources(self, source_arn) + dlq_queue_arn, dlq_resources = EventBridgeRuleUtils.get_dlq_queue_arn_and_resources( + self, source_arn, passthrough_resource_attributes + ) resources.extend(dlq_resources) events_rule.Targets = [self._construct_target(function, dlq_queue_arn)] - if CONDITION in function.resource_attributes: - events_rule.set_resource_attribute(CONDITION, function.resource_attributes[CONDITION]) resources.append(events_rule) resources.append(self._construct_permission(function, source_arn=source_arn)) @@ -427,20 +429,20 @@ def to_cloudformation(self, **kwargs): self.Topic, self.Region, self.FilterPolicy, - function.resource_attributes, + function, ) return [self._construct_permission(function, source_arn=self.Topic), subscription] # SNS -> SQS(Create New) -> Lambda if isinstance(self.SqsSubscription, bool): resources = [] - queue = self._inject_sqs_queue() + queue = self._inject_sqs_queue(function) queue_arn = queue.get_runtime_attr("arn") queue_url = queue.get_runtime_attr("queue_url") - queue_policy = self._inject_sqs_queue_policy(self.Topic, queue_arn, queue_url, function.resource_attributes) + queue_policy = self._inject_sqs_queue_policy(self.Topic, queue_arn, queue_url, function) subscription = self._inject_subscription( - "sqs", queue_arn, self.Topic, self.Region, self.FilterPolicy, function.resource_attributes + "sqs", queue_arn, self.Topic, self.Region, self.FilterPolicy, function ) event_source = self._inject_sqs_event_source_mapping(function, role, queue_arn) @@ -462,11 +464,9 @@ def to_cloudformation(self, **kwargs): enabled = self.SqsSubscription.get("Enabled", None) queue_policy = self._inject_sqs_queue_policy( - self.Topic, queue_arn, queue_url, function.resource_attributes, queue_policy_logical_id - ) - subscription = self._inject_subscription( - "sqs", queue_arn, self.Topic, self.Region, self.FilterPolicy, function.resource_attributes + self.Topic, queue_arn, queue_url, function, queue_policy_logical_id ) + subscription = self._inject_subscription("sqs", queue_arn, self.Topic, self.Region, self.FilterPolicy, function) event_source = self._inject_sqs_event_source_mapping(function, role, queue_arn, batch_size, enabled) resources = resources + event_source @@ -474,35 +474,36 @@ def to_cloudformation(self, **kwargs): resources.append(subscription) return resources - def _inject_subscription(self, protocol, endpoint, topic, region, filterPolicy, resource_attributes): - subscription = SNSSubscription(self.logical_id) + def _inject_subscription(self, protocol, endpoint, topic, region, filterPolicy, function): + subscription = SNSSubscription(self.logical_id, attributes=function.get_passthrough_resource_attributes()) subscription.Protocol = protocol subscription.Endpoint = endpoint subscription.TopicArn = topic + if region is not None: subscription.Region = region - if CONDITION in resource_attributes: - subscription.set_resource_attribute(CONDITION, resource_attributes[CONDITION]) if filterPolicy is not None: subscription.FilterPolicy = filterPolicy return subscription - def _inject_sqs_queue(self): - return SQSQueue(self.logical_id + "Queue") + def _inject_sqs_queue(self, function): + return SQSQueue(self.logical_id + "Queue", attributes=function.get_passthrough_resource_attributes()) def _inject_sqs_event_source_mapping(self, function, role, queue_arn, batch_size=None, enabled=None): - event_source = SQS(self.logical_id + "EventSourceMapping") + event_source = SQS( + self.logical_id + "EventSourceMapping", attributes=function.get_passthrough_resource_attributes() + ) event_source.Queue = queue_arn event_source.BatchSize = batch_size or 10 event_source.Enabled = enabled or True return event_source.to_cloudformation(function=function, role=role) - def _inject_sqs_queue_policy(self, topic_arn, queue_arn, queue_url, resource_attributes, logical_id=None): - policy = SQSQueuePolicy(logical_id or self.logical_id + "QueuePolicy") - if CONDITION in resource_attributes: - policy.set_resource_attribute(CONDITION, resource_attributes[CONDITION]) + def _inject_sqs_queue_policy(self, topic_arn, queue_arn, queue_url, function, logical_id=None): + policy = SQSQueuePolicy( + logical_id or self.logical_id + "QueuePolicy", attributes=function.get_passthrough_resource_attributes() + ) policy.PolicyDocument = SQSQueuePolicies.sns_topic_send_message_role_policy(topic_arn, queue_arn) policy.Queues = [queue_url] @@ -895,7 +896,7 @@ def to_cloudformation(self, **kwargs): return resources def _construct_iot_rule(self, function): - rule = IotTopicRule(self.logical_id) + rule = IotTopicRule(self.logical_id, attributes=function.get_passthrough_resource_attributes()) payload = { "Sql": self.Sql, @@ -907,8 +908,6 @@ def _construct_iot_rule(self, function): payload["AwsIotSqlVersion"] = self.AwsIotSqlVersion rule.TopicRulePayload = payload - if CONDITION in function.resource_attributes: - rule.set_resource_attribute(CONDITION, function.resource_attributes[CONDITION]) return rule @@ -953,11 +952,17 @@ def to_cloudformation(self, **kwargs): resources = [] source_arn = fnGetAtt(userpool_id, "Arn") - resources.append( - self._construct_permission(function, source_arn=source_arn, prefix=function.logical_id + "Cognito") + lambda_permission = self._construct_permission( + function, source_arn=source_arn, prefix=function.logical_id + "Cognito" ) + for attribute, value in function.get_passthrough_resource_attributes().items(): + lambda_permission.set_resource_attribute(attribute, value) + resources.append(lambda_permission) self._inject_lambda_config(function, userpool) + userpool_resource = CognitoUserPool.from_dict(userpool_id, userpool) + for attribute, value in function.get_passthrough_resource_attributes().items(): + userpool_resource.set_resource_attribute(attribute, value) resources.append(CognitoUserPool.from_dict(userpool_id, userpool)) return resources diff --git a/samtranslator/model/sam_resources.py b/samtranslator/model/sam_resources.py index 66823b93d..b0d179c2f 100644 --- a/samtranslator/model/sam_resources.py +++ b/samtranslator/model/sam_resources.py @@ -1,5 +1,6 @@ """ SAM macro definitions """ from six import string_types +import copy import samtranslator.model.eventsources import samtranslator.model.eventsources.pull @@ -279,13 +280,17 @@ def _validate_and_inject_resource(self, dest_config, event, logical_id, conditio ) if dest_config.get("Destination") is None or property_condition is not None: combined_condition = self._make_and_conditions( - self.get_passthrough_resource_attributes(), property_condition, conditions + self.get_passthrough_resource_attributes().get("Condition"), property_condition, conditions ) if dest_config.get("Type") in auto_inject_list: if dest_config.get("Type") == "SQS": - resource = SQSQueue(resource_logical_id + "Queue") + resource = SQSQueue( + resource_logical_id + "Queue", attributes=self.get_passthrough_resource_attributes() + ) if dest_config.get("Type") == "SNS": - resource = SNSTopic(resource_logical_id + "Topic") + resource = SNSTopic( + resource_logical_id + "Topic", attributes=self.get_passthrough_resource_attributes() + ) if combined_condition: resource.set_resource_attribute("Condition", combined_condition) if property_condition: @@ -313,12 +318,10 @@ def _make_and_conditions(self, resource_condition, property_condition, condition return property_condition if property_condition is None: - return resource_condition["Condition"] + return resource_condition - and_condition = make_and_condition([resource_condition, {"Condition": property_condition}]) - condition_name = self._make_gen_condition_name( - resource_condition.get("Condition") + "AND" + property_condition, self.logical_id - ) + and_condition = make_and_condition([{"Condition": resource_condition}, {"Condition": property_condition}]) + condition_name = self._make_gen_condition_name(resource_condition + "AND" + property_condition, self.logical_id) conditions[condition_name] = and_condition return condition_name @@ -732,7 +735,8 @@ def _construct_version(self, function, intrinsics_resolver, code_sha256=None): attributes = self.get_passthrough_resource_attributes() if attributes is None: attributes = {} - attributes["DeletionPolicy"] = "Retain" + if "DeletionPolicy" not in attributes: + attributes["DeletionPolicy"] = "Retain" lambda_version = LambdaVersion(logical_id=logical_id, attributes=attributes) lambda_version.FunctionName = function.get_runtime_attr("name") @@ -1162,15 +1166,29 @@ def _construct_lambda_layer(self, intrinsics_resolver): intrinsics_resolver, self.RetentionPolicy, "RetentionPolicy" ) + # If nothing defined, this will be set to Retain retention_policy_value = self._get_retention_policy_value() attributes = self.get_passthrough_resource_attributes() if attributes is None: attributes = {} - attributes["DeletionPolicy"] = retention_policy_value + if "DeletionPolicy" not in attributes: + attributes["DeletionPolicy"] = self.RETAIN + if retention_policy_value is not None: + attributes["DeletionPolicy"] = retention_policy_value old_logical_id = self.logical_id - new_logical_id = logical_id_generator.LogicalIdGenerator(old_logical_id, self.to_dict()).gen() + + # This is to prevent the passthrough resource attributes to be included for hashing + hash_dict = copy.deepcopy(self.to_dict()) + if "DeletionPolicy" in hash_dict.get(old_logical_id): + del hash_dict[old_logical_id]["DeletionPolicy"] + if "UpdateReplacePolicy" in hash_dict.get(old_logical_id): + del hash_dict[old_logical_id]["UpdateReplacePolicy"] + if "Metadata" in hash_dict.get(old_logical_id): + del hash_dict[old_logical_id]["Metadata"] + + new_logical_id = logical_id_generator.LogicalIdGenerator(old_logical_id, hash_dict).gen() self.logical_id = new_logical_id lambda_layer = LambdaLayerVersion(self.logical_id, depends_on=self.depends_on, attributes=attributes) @@ -1202,7 +1220,9 @@ def _get_retention_policy_value(self): :return: value for the DeletionPolicy attribute. """ - if self.RetentionPolicy is None or self.RetentionPolicy.lower() == self.RETAIN.lower(): + if self.RetentionPolicy is None: + return None + elif self.RetentionPolicy.lower() == self.RETAIN.lower(): return self.RETAIN elif self.RetentionPolicy.lower() == self.DELETE.lower(): return self.DELETE diff --git a/samtranslator/model/stepfunctions/events.py b/samtranslator/model/stepfunctions/events.py index 88fe60cfe..a50054528 100644 --- a/samtranslator/model/stepfunctions/events.py +++ b/samtranslator/model/stepfunctions/events.py @@ -97,7 +97,8 @@ def to_cloudformation(self, resource, **kwargs): permissions_boundary = kwargs.get("permissions_boundary") - events_rule = EventsRule(self.logical_id) + passthrough_resource_attributes = resource.get_passthrough_resource_attributes() + events_rule = EventsRule(self.logical_id, attributes=passthrough_resource_attributes) resources.append(events_rule) events_rule.ScheduleExpression = self.Schedule @@ -105,8 +106,6 @@ def to_cloudformation(self, resource, **kwargs): events_rule.State = "ENABLED" if self.Enabled else "DISABLED" events_rule.Name = self.Name events_rule.Description = self.Description - if CONDITION in resource.resource_attributes: - events_rule.set_resource_attribute(CONDITION, resource.resource_attributes[CONDITION]) role = self._construct_role(resource, permissions_boundary) resources.append(role) @@ -115,7 +114,9 @@ def to_cloudformation(self, resource, **kwargs): dlq_queue_arn = None if self.DeadLetterConfig is not None: EventBridgeRuleUtils.validate_dlq_config(self.logical_id, self.DeadLetterConfig) - dlq_queue_arn, dlq_resources = EventBridgeRuleUtils.get_dlq_queue_arn_and_resources(self, source_arn) + dlq_queue_arn, dlq_resources = EventBridgeRuleUtils.get_dlq_queue_arn_and_resources( + self, source_arn, passthrough_resource_attributes + ) resources.extend(dlq_resources) events_rule.Targets = [self._construct_target(resource, role, dlq_queue_arn)] @@ -170,11 +171,10 @@ def to_cloudformation(self, resource, **kwargs): permissions_boundary = kwargs.get("permissions_boundary") - events_rule = EventsRule(self.logical_id) + passthrough_resource_attributes = resource.get_passthrough_resource_attributes() + events_rule = EventsRule(self.logical_id, attributes=passthrough_resource_attributes) events_rule.EventBusName = self.EventBusName events_rule.EventPattern = self.Pattern - if CONDITION in resource.resource_attributes: - events_rule.set_resource_attribute(CONDITION, resource.resource_attributes[CONDITION]) resources.append(events_rule) @@ -185,7 +185,9 @@ def to_cloudformation(self, resource, **kwargs): dlq_queue_arn = None if self.DeadLetterConfig is not None: EventBridgeRuleUtils.validate_dlq_config(self.logical_id, self.DeadLetterConfig) - dlq_queue_arn, dlq_resources = EventBridgeRuleUtils.get_dlq_queue_arn_and_resources(self, source_arn) + dlq_queue_arn, dlq_resources = EventBridgeRuleUtils.get_dlq_queue_arn_and_resources( + self, source_arn, passthrough_resource_attributes + ) resources.extend(dlq_resources) events_rule.Targets = [self._construct_target(resource, role, dlq_queue_arn)] diff --git a/samtranslator/plugins/api/implicit_api_plugin.py b/samtranslator/plugins/api/implicit_api_plugin.py index 2ae336103..abf440b08 100644 --- a/samtranslator/plugins/api/implicit_api_plugin.py +++ b/samtranslator/plugins/api/implicit_api_plugin.py @@ -39,6 +39,8 @@ def __init__(self, name): # dict containing condition (or None) for each resource path+method for all APIs. dict format: # {api_id: {path: {method: condition_name_or_None}}} self.api_conditions = {} + self.api_deletion_policies = {} + self.api_update_replace_policies = {} self._setup_api_properties() def _setup_api_properties(self): @@ -75,16 +77,22 @@ def on_before_transform_template(self, template_dict): api_events = self._get_api_events(resource) condition = resource.condition + deletion_policy = resource.deletion_policy + update_replace_policy = resource.update_replace_policy if len(api_events) == 0: continue try: - self._process_api_events(resource, api_events, template, condition) + self._process_api_events( + resource, api_events, template, condition, deletion_policy, update_replace_policy + ) except InvalidEventException as ex: errors.append(InvalidResourceException(logicalId, ex.message)) self._maybe_add_condition_to_implicit_api(template_dict) + self._maybe_add_deletion_policy_to_implicit_api(template_dict) + self._maybe_add_update_replace_policy_to_implicit_api(template_dict) self._maybe_add_conditions_to_implicit_api_paths(template) self._maybe_remove_implicit_api(template) @@ -121,7 +129,9 @@ def _get_api_events(self, resource): return api_events - def _process_api_events(self, resource, api_events, template, condition=None): + def _process_api_events( + self, resource, api_events, template, condition=None, deletion_policy=None, update_replace_policy=None + ): """ Actually process given API events. Iteratively adds the APIs to Swagger JSON in the respective Serverless::Api resource from the template @@ -247,6 +257,75 @@ def _maybe_add_condition_to_implicit_api(self, template_dict): template_dict, self.implicit_api_condition, all_resource_method_conditions ) + def _maybe_add_deletion_policy_to_implicit_api(self, template_dict): + """ + Decides whether to add a deletion policy to the implicit api resource. + :param dict template_dict: SAM template dictionary + """ + # Short-circuit if template doesn't have any functions with implicit API events + if not self.api_deletion_policies.get(self.implicit_api_logical_id, {}): + return + + # Add a deletion policy to the API resource if its resources contains DeletionPolicy. + implicit_api_deletion_policies = self.api_deletion_policies.get(self.implicit_api_logical_id) + at_least_one_resource_method = len(implicit_api_deletion_policies) > 0 + one_resource_method_contains_deletion_policy = False + contains_retain = False + contains_delete = False + # If multiple functions with multiple different policies reference the Implicit Api, + # we set DeletionPolicy to Retain if Retain is present in one of the functions, + # else Delete if Delete is present + for iterated_policy in implicit_api_deletion_policies: + if iterated_policy: + one_resource_method_contains_deletion_policy = True + if iterated_policy == "Retain": + contains_retain = True + if iterated_policy == "Delete": + contains_delete = True + if at_least_one_resource_method and one_resource_method_contains_deletion_policy: + implicit_api_resource = template_dict.get("Resources").get(self.implicit_api_logical_id) + if contains_retain: + implicit_api_resource["DeletionPolicy"] = "Retain" + elif contains_delete: + implicit_api_resource["DeletionPolicy"] = "Delete" + + def _maybe_add_update_replace_policy_to_implicit_api(self, template_dict): + """ + Decides whether to add an update replace policy to the implicit api resource. + :param dict template_dict: SAM template dictionary + """ + # Short-circuit if template doesn't have any functions with implicit API events + if not self.api_update_replace_policies.get(self.implicit_api_logical_id, {}): + return + + # Add a update replace policy to the API resource if its resources contains UpdateReplacePolicy. + implicit_api_update_replace_policies = self.api_update_replace_policies.get(self.implicit_api_logical_id) + at_least_one_resource_method = len(implicit_api_update_replace_policies) > 0 + one_resource_method_contains_update_replace_policy = False + contains_retain = False + contains_snapshot = False + contains_delete = False + # If multiple functions with multiple different policies reference the Implicit Api, + # we set UpdateReplacePolicy to Retain if Retain is present in one of the functions, + # Snapshot if Snapshot is present, else Delete if Delete is present + for iterated_policy in implicit_api_update_replace_policies: + if iterated_policy: + one_resource_method_contains_update_replace_policy = True + if iterated_policy == "Retain": + contains_retain = True + if iterated_policy == "Snapshot": + contains_snapshot = True + if iterated_policy == "Delete": + contains_delete = True + if at_least_one_resource_method and one_resource_method_contains_update_replace_policy: + implicit_api_resource = template_dict.get("Resources").get(self.implicit_api_logical_id) + if contains_retain: + implicit_api_resource["UpdateReplacePolicy"] = "Retain" + elif contains_snapshot: + implicit_api_resource["UpdateReplacePolicy"] = "Snapshot" + elif contains_delete: + implicit_api_resource["UpdateReplacePolicy"] = "Delete" + def _add_combined_condition_to_template(self, template_dict, condition_name, conditions_to_combine): """ Add top-level template condition that combines the given list of conditions. diff --git a/samtranslator/plugins/api/implicit_http_api_plugin.py b/samtranslator/plugins/api/implicit_http_api_plugin.py index 83c078ea1..372742632 100644 --- a/samtranslator/plugins/api/implicit_http_api_plugin.py +++ b/samtranslator/plugins/api/implicit_http_api_plugin.py @@ -43,7 +43,9 @@ def _setup_api_properties(self): self.api_id_property = "ApiId" self.editor = OpenApiEditor - def _process_api_events(self, function, api_events, template, condition=None): + def _process_api_events( + self, function, api_events, template, condition=None, deletion_policy=None, update_replace_policy=None + ): """ Actually process given HTTP API events. Iteratively adds the APIs to OpenApi JSON in the respective AWS::Serverless::HttpApi resource from the template @@ -89,11 +91,15 @@ def _process_api_events(self, function, api_events, template, condition=None): if isinstance(api_id, dict): raise InvalidEventException(logicalId, "Api Event must reference an Api in the same template.") - api_dict = self.api_conditions.setdefault(api_id, {}) - method_conditions = api_dict.setdefault(path, {}) + api_dict_condition = self.api_conditions.setdefault(api_id, {}) + method_conditions = api_dict_condition.setdefault(path, {}) + method_conditions[method] = condition - if condition: - method_conditions[method] = condition + api_dict_deletion = self.api_deletion_policies.setdefault(api_id, set()) + api_dict_deletion.add(deletion_policy) + + api_dict_update_replace = self.api_update_replace_policies.setdefault(api_id, set()) + api_dict_update_replace.add(update_replace_policy) self._add_api_to_swagger(logicalId, event_properties, template) if "RouteSettings" in event_properties: diff --git a/samtranslator/plugins/api/implicit_rest_api_plugin.py b/samtranslator/plugins/api/implicit_rest_api_plugin.py index 92ecf8d86..3e94309f1 100644 --- a/samtranslator/plugins/api/implicit_rest_api_plugin.py +++ b/samtranslator/plugins/api/implicit_rest_api_plugin.py @@ -46,7 +46,9 @@ def _setup_api_properties(self): self.api_id_property = "RestApiId" self.editor = SwaggerEditor - def _process_api_events(self, function, api_events, template, condition=None): + def _process_api_events( + self, function, api_events, template, condition=None, deletion_policy=None, update_replace_policy=None + ): """ Actually process given API events. Iteratively adds the APIs to Swagger JSON in the respective Serverless::Api resource from the template @@ -87,10 +89,16 @@ def _process_api_events(self, function, api_events, template, condition=None): if isinstance(api_id, dict): raise InvalidEventException(logicalId, "Api Event must reference an Api in the same template.") - api_dict = self.api_conditions.setdefault(api_id, {}) - method_conditions = api_dict.setdefault(path, {}) + api_dict_condition = self.api_conditions.setdefault(api_id, {}) + method_conditions = api_dict_condition.setdefault(path, {}) method_conditions[method] = condition + api_dict_deletion = self.api_deletion_policies.setdefault(api_id, set()) + api_dict_deletion.add(deletion_policy) + + api_dict_update_replace = self.api_update_replace_policies.setdefault(api_id, set()) + api_dict_update_replace.add(update_replace_policy) + self._add_api_to_swagger(logicalId, event_properties, template) api_events[logicalId] = event diff --git a/samtranslator/sdk/resource.py b/samtranslator/sdk/resource.py index d430feaa6..94b310281 100644 --- a/samtranslator/sdk/resource.py +++ b/samtranslator/sdk/resource.py @@ -23,6 +23,8 @@ def __init__(self, resource_dict): self.resource_dict = resource_dict self.type = resource_dict.get("Type") self.condition = resource_dict.get("Condition", None) + self.deletion_policy = resource_dict.get("DeletionPolicy", None) + self.update_replace_policy = resource_dict.get("UpdateReplacePolicy", None) # Properties is *not* required. Ex: SimpleTable resource has no required properties self.properties = resource_dict.get("Properties", {}) @@ -41,6 +43,20 @@ def valid(self): if not is_str()(self.condition, should_raise=False): raise InvalidDocumentException([InvalidTemplateException("Every Condition member must be a string.")]) + if self.deletion_policy: + + if not is_str()(self.deletion_policy, should_raise=False): + raise InvalidDocumentException( + [InvalidTemplateException("Every DeletionPolicy member must be a string.")] + ) + + if self.update_replace_policy: + + if not is_str()(self.update_replace_policy, should_raise=False): + raise InvalidDocumentException( + [InvalidTemplateException("Every UpdateReplacePolicy member must be a string.")] + ) + return SamResourceType.has_value(self.type) def to_dict(self): diff --git a/samtranslator/validator/sam_schema/schema.json b/samtranslator/validator/sam_schema/schema.json index 72df7539e..4cf7f1f5c 100644 --- a/samtranslator/validator/sam_schema/schema.json +++ b/samtranslator/validator/sam_schema/schema.json @@ -6,11 +6,12 @@ "additionalProperties": false, "properties": { "DeletionPolicy": { - "enum": [ - "Delete", - "Retain", - "Snapshot" - ], + "type": "string" + }, + "UpdateReplacePolicy": { + "type": "string" + }, + "Condition": { "type": "string" }, "DependsOn": { @@ -132,11 +133,12 @@ "additionalProperties": false, "properties": { "DeletionPolicy": { - "enum": [ - "Delete", - "Retain", - "Snapshot" - ], + "type": "string" + }, + "UpdateReplacePolicy": { + "type": "string" + }, + "Condition": { "type": "string" }, "DependsOn": { @@ -838,11 +840,12 @@ "additionalProperties": false, "properties": { "DeletionPolicy": { - "enum": [ - "Delete", - "Retain", - "Snapshot" - ], + "type": "string" + }, + "UpdateReplacePolicy": { + "type": "string" + }, + "Condition": { "type": "string" }, "DependsOn": { diff --git a/tests/model/test_sam_resources.py b/tests/model/test_sam_resources.py index e84c5b1ce..f37d82b05 100644 --- a/tests/model/test_sam_resources.py +++ b/tests/model/test_sam_resources.py @@ -332,3 +332,11 @@ def test_with_description_not_defined_in_definition_body(self): resources = sam_http_api.to_cloudformation(**self.kwargs) http_api = [x for x in resources if isinstance(x, ApiGatewayV2HttpApi)] self.assertEqual(http_api[0].Body.get("info", {}).get("description"), "new description") + + +class TestPassthroughResourceAttributes(TestCase): + def test_with_passthrough_resource_attributes(self): + expected = {"DeletionPolicy": "Delete", "UpdateReplacePolicy": "Retain", "Condition": "C1"} + function = SamFunction("foo", attributes=expected) + attributes = function.get_passthrough_resource_attributes() + self.assertEqual(attributes, expected) diff --git a/tests/plugins/api/test_implicit_api_plugin.py b/tests/plugins/api/test_implicit_api_plugin.py index 3181e11bb..1ad927212 100644 --- a/tests/plugins/api/test_implicit_api_plugin.py +++ b/tests/plugins/api/test_implicit_api_plugin.py @@ -96,9 +96,9 @@ def test_must_process_functions(self, SamTemplateMock): self.plugin._get_api_events.assert_has_calls([call(function1), call(function2), call(function3)]) self.plugin._process_api_events.assert_has_calls( [ - call(function1, ["event1", "event2"], sam_template, None), - call(function2, ["event1", "event2"], sam_template, None), - call(function3, ["event1", "event2"], sam_template, None), + call(function1, ["event1", "event2"], sam_template, None, None, None), + call(function2, ["event1", "event2"], sam_template, None, None, None), + call(function3, ["event1", "event2"], sam_template, None, None, None), ] ) @@ -133,9 +133,9 @@ def test_must_process_state_machines(self, SamTemplateMock): self.plugin._get_api_events.assert_has_calls([call(statemachine1), call(statemachine2), call(statemachine3)]) self.plugin._process_api_events.assert_has_calls( [ - call(statemachine1, ["event1", "event2"], sam_template, None), - call(statemachine2, ["event1", "event2"], sam_template, None), - call(statemachine3, ["event1", "event2"], sam_template, None), + call(statemachine1, ["event1", "event2"], sam_template, None, None, None), + call(statemachine2, ["event1", "event2"], sam_template, None, None, None), + call(statemachine3, ["event1", "event2"], sam_template, None, None, None), ] ) @@ -813,3 +813,38 @@ def test_must_restore_if_existing_resource_present(self): # Must restore original resource template.set.assert_called_with(IMPLICIT_API_LOGICAL_ID, resource) + + +class TestImplicitApiPlugin_generate_resource_attributes(TestCase): + def setUp(self): + self.plugin = ImplicitRestApiPlugin() + self.plugin.api_conditions = {} + + def test_maybe_add_condition(self): + template_dict = {"Resources": {"ServerlessRestApi": {"Type": "AWS::Serverless::Api"}}} + self.plugin.api_conditions = {"ServerlessRestApi": {"/{proxy+}": {"any": "C1"}}} + self.plugin._maybe_add_condition_to_implicit_api(template_dict) + print(template_dict) + self.assertEqual( + template_dict, {"Resources": {"ServerlessRestApi": {"Type": "AWS::Serverless::Api", "Condition": "C1"}}} + ) + + def test_maybe_add_deletion_policies(self): + template_dict = {"Resources": {"ServerlessRestApi": {"Type": "AWS::Serverless::Api"}}} + self.plugin.api_deletion_policies = {"ServerlessRestApi": {"Delete", "Retain"}} + self.plugin._maybe_add_deletion_policy_to_implicit_api(template_dict) + print(template_dict) + self.assertEqual( + template_dict, + {"Resources": {"ServerlessRestApi": {"Type": "AWS::Serverless::Api", "DeletionPolicy": "Retain"}}}, + ) + + def test_maybe_add_update_replace_policies(self): + template_dict = {"Resources": {"ServerlessRestApi": {"Type": "AWS::Serverless::Api"}}} + self.plugin.api_update_replace_policies = {"ServerlessRestApi": {"Snapshot", "Retain"}} + self.plugin._maybe_add_update_replace_policy_to_implicit_api(template_dict) + print(template_dict) + self.assertEqual( + template_dict, + {"Resources": {"ServerlessRestApi": {"Type": "AWS::Serverless::Api", "UpdateReplacePolicy": "Retain"}}}, + ) diff --git a/tests/test_model.py b/tests/test_model.py index 84da811b8..9783be7b5 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -107,6 +107,15 @@ def test_to_dict(self): dict_with_attributes = { "id": {"Type": "foo", "Properties": {}, "UpdatePolicy": "update", "DeletionPolicy": {"foo": "bar"}} } + dict_with_attributes2 = { + "id": { + "Type": "foo", + "Properties": {}, + "UpdateReplacePolicy": "update", + "Metadata": {"foo": "bar"}, + "Condition": "con", + } + } r = self.MyResource("id") self.assertEqual(r.to_dict(), empty_resource_dict) @@ -114,6 +123,11 @@ def test_to_dict(self): r = self.MyResource("id", attributes={"UpdatePolicy": "update", "DeletionPolicy": {"foo": "bar"}}) self.assertEqual(r.to_dict(), dict_with_attributes) + r = self.MyResource( + "id", attributes={"UpdateReplacePolicy": "update", "Metadata": {"foo": "bar"}, "Condition": "con"} + ) + self.assertEqual(r.to_dict(), dict_with_attributes2) + def test_invalid_attr(self): with pytest.raises(KeyError) as ex: @@ -140,6 +154,9 @@ def test_from_dict(self): "Properties": {}, "UpdatePolicy": "update", "DeletionPolicy": [1, 2, 3], + "UpdateReplacePolicy": "update", + "Metadata": {"foo": "bar"}, + "Condition": "con", } r = self.MyResource.from_dict("id", resource_dict=no_attribute) @@ -148,6 +165,9 @@ def test_from_dict(self): r = self.MyResource.from_dict("id", resource_dict=all_supported_attributes) self.assertEqual(r.get_resource_attribute("DeletionPolicy"), [1, 2, 3]) self.assertEqual(r.get_resource_attribute("UpdatePolicy"), "update") + self.assertEqual(r.get_resource_attribute("UpdateReplacePolicy"), "update") + self.assertEqual(r.get_resource_attribute("Metadata"), {"foo": "bar"}) + self.assertEqual(r.get_resource_attribute("Condition"), "con") class TestResourceRuntimeAttributes(TestCase): diff --git a/tests/translator/input/api_with_swagger_authorizer_none.yaml b/tests/translator/input/api_with_swagger_authorizer_none.yaml new file mode 100644 index 000000000..eb0ae32be --- /dev/null +++ b/tests/translator/input/api_with_swagger_authorizer_none.yaml @@ -0,0 +1,117 @@ +Resources: + MyApiWithCognitoAuth: + Type: "AWS::Serverless::Api" + Properties: + StageName: Prod + Auth: + Authorizers: + MyCognitoAuth: + UserPoolArn: !GetAtt MyUserPool.Arn + DefaultAuthorizer: MyCognitoAuth + + MyApiWithLambdaTokenAuth: + Type: "AWS::Serverless::Api" + Properties: + StageName: Prod + Auth: + Authorizers: + MyLambdaTokenAuth: + FunctionArn: !GetAtt MyAuthFn.Arn + DefaultAuthorizer: MyLambdaTokenAuth + + MyApiWithLambdaRequestAuth: + Type: "AWS::Serverless::Api" + Properties: + StageName: Prod + DefinitionBody: + swagger: 2.0 + info: + version: '1.0' + title: !Ref AWS::StackName + schemes: + - https + paths: + "/lambda-request": + get: + x-amazon-apigateway-integration: + httpMethod: POST + type: aws_proxy + uri: !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${MyFn.Arn}/invocations + passthroughBehavior: when_no_match + responses: {} + Auth: + Authorizers: + MyLambdaRequestAuth: + FunctionPayloadType: REQUEST + FunctionArn: !GetAtt MyAuthFn.Arn + Identity: + Headers: + - Authorization1 + DefaultAuthorizer: MyLambdaRequestAuth + + MyAuthFn: + Type: AWS::Serverless::Function + Properties: + InlineCode: | + exports.handler = async (event) => { + return { + statusCode: 200, + body: JSON.stringify(event), + headers: {} + } + } + Handler: index.handler + Runtime: nodejs8.10 + + MyFn: + Type: AWS::Serverless::Function + Properties: + InlineCode: | + exports.handler = async (event) => { + return { + statusCode: 200, + body: JSON.stringify(event), + headers: {} + } + } + Handler: index.handler + Runtime: nodejs8.10 + Events: + Cognito: + Type: Api + Properties: + RestApiId: !Ref MyApiWithCognitoAuth + Method: get + Auth: + Authorizer: NONE + Path: /cognito + LambdaToken: + Type: Api + Properties: + RestApiId: !Ref MyApiWithLambdaTokenAuth + Method: get + Auth: + Authorizer: NONE + Path: /lambda-token + LambdaRequest: + Type: Api + Properties: + RestApiId: !Ref MyApiWithLambdaRequestAuth + Auth: + Authorizer: NONE + Method: get + Path: /lambda-request + + MyUserPool: + Type: AWS::Cognito::UserPool + Properties: + UserPoolName: UserPoolName + Policies: + PasswordPolicy: + MinimumLength: 8 + UsernameAttributes: + - email + Schema: + - AttributeDataType: String + Name: email + Required: false \ No newline at end of file diff --git a/tests/translator/input/implicit_api_deletion_policy_precedence.yaml b/tests/translator/input/implicit_api_deletion_policy_precedence.yaml new file mode 100644 index 000000000..643b9ac47 --- /dev/null +++ b/tests/translator/input/implicit_api_deletion_policy_precedence.yaml @@ -0,0 +1,32 @@ +Resources: + RestApiFunction: + Type: AWS::Serverless::Function + DeletionPolicy: Delete + UpdateReplacePolicy: Retain + Properties: + CodeUri: s3://sam-demo-bucket/todo_list.zip + Handler: index.restapi + Runtime: nodejs12.x + Policies: AmazonDynamoDBFullAccess + Events: + GetHtml: + Type: Api + Properties: + Path: /{proxy+} + Method: any + + GetHtmlFunction: + Type: AWS::Serverless::Function + DeletionPolicy: Retain + UpdateReplacePolicy: Retain + Properties: + CodeUri: s3://sam-demo-bucket/todo_list.zip + Handler: index.gethtml + Runtime: nodejs12.x + Policies: AmazonDynamoDBReadOnlyAccess + Events: + GetHtml: + Type: Api + Properties: + Path: /{proxy++} + Method: any diff --git a/tests/translator/input/layer_deletion_policy_precedence.yaml b/tests/translator/input/layer_deletion_policy_precedence.yaml new file mode 100644 index 000000000..a967ed621 --- /dev/null +++ b/tests/translator/input/layer_deletion_policy_precedence.yaml @@ -0,0 +1,18 @@ +Resources: + MinimalLayer: + Type: 'AWS::Serverless::LayerVersion' + DeletionPolicy: Delete + Properties: + ContentUri: s3://sam-demo-bucket/layer.zip + RetentionPolicy: Retain + + MinimalLayer2: + Type: 'AWS::Serverless::LayerVersion' + DeletionPolicy: Delete + Properties: + ContentUri: s3://sam-demo-bucket/layer.zip + + MinimalLayer3: + Type: 'AWS::Serverless::LayerVersion' + Properties: + ContentUri: s3://sam-demo-bucket/layer.zip \ No newline at end of file diff --git a/tests/translator/input/version_deletion_policy_precedence.yaml b/tests/translator/input/version_deletion_policy_precedence.yaml new file mode 100644 index 000000000..bf868f9a6 --- /dev/null +++ b/tests/translator/input/version_deletion_policy_precedence.yaml @@ -0,0 +1,19 @@ +Resources: + MinimalFunction: + Type: 'AWS::Serverless::Function' + Properties: + CodeUri: s3://sam-demo-bucket/hello.zip + Handler: hello.handler + Runtime: python2.7 + AutoPublishAlias: live + VersionDescription: sam-testing + + MinimalFunction2: + Type: 'AWS::Serverless::Function' + DeletionPolicy: Delete + Properties: + CodeUri: s3://sam-demo-bucket/hello.zip + Handler: hello.handler + Runtime: python2.7 + AutoPublishAlias: live + VersionDescription: sam-testing \ No newline at end of file diff --git a/tests/translator/output/api_with_swagger_authorizer_none.json b/tests/translator/output/api_with_swagger_authorizer_none.json index d9b372b88..be6d32c56 100644 --- a/tests/translator/output/api_with_swagger_authorizer_none.json +++ b/tests/translator/output/api_with_swagger_authorizer_none.json @@ -1,475 +1,475 @@ { - "Resources": { - "MyFnCognitoPermissionProd": { - "Type": "AWS::Lambda::Permission", - "Properties": { - "Action": "lambda:InvokeFunction", - "Principal": "apigateway.amazonaws.com", - "FunctionName": { - "Ref": "MyFn" - }, - "SourceArn": { - "Fn::Sub": [ - "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${__ApiId__}/${__Stage__}/GET/cognito", - { - "__Stage__": "*", - "__ApiId__": { - "Ref": "MyApiWithCognitoAuth" + "Resources": { + "MyFnCognitoPermissionProd": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "Principal": "apigateway.amazonaws.com", + "FunctionName": { + "Ref": "MyFn" + }, + "SourceArn": { + "Fn::Sub": [ + "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${__ApiId__}/${__Stage__}/GET/cognito", + { + "__Stage__": "*", + "__ApiId__": { + "Ref": "MyApiWithCognitoAuth" + } } - } - ] + ] + } } - } - }, - "MyApiWithCognitoAuth": { - "Type": "AWS::ApiGateway::RestApi", - "Properties": { - "Body": { - "info": { - "version": "1.0", - "title": { - "Ref": "AWS::StackName" - } - }, - "paths": { - "/cognito": { - "get": { - "x-amazon-apigateway-integration": { - "httpMethod": "POST", - "type": "aws_proxy", - "uri": { - "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${MyFn.Arn}/invocations" - } + }, + "MyApiWithCognitoAuth": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "Body": { + "info": { + "version": "1.0", + "title": { + "Ref": "AWS::StackName" + } + }, + "paths": { + "/cognito": { + "get": { + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "type": "aws_proxy", + "uri": { + "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${MyFn.Arn}/invocations" + } + }, + "security": [ + { + "NONE": [] + } + ], + "responses": {} + } + } + }, + "swagger": "2.0", + "securityDefinitions": { + "MyCognitoAuth": { + "in": "header", + "type": "apiKey", + "name": "Authorization", + "x-amazon-apigateway-authorizer": { + "providerARNs": [ + { + "Fn::GetAtt": [ + "MyUserPool", + "Arn" + ] + } + ], + "type": "cognito_user_pools" }, - "security": [ - { - "NONE": [] - } - ], - "responses": {} + "x-amazon-apigateway-authtype": "cognito_user_pools" } } + } + } + }, + "MyApiWithLambdaRequestAuthMyLambdaRequestAuthAuthorizerPermission": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "Principal": "apigateway.amazonaws.com", + "FunctionName": { + "Fn::GetAtt": [ + "MyAuthFn", + "Arn" + ] }, - "swagger": "2.0", - "securityDefinitions": { - "MyCognitoAuth": { - "in": "header", - "type": "apiKey", - "name": "Authorization", - "x-amazon-apigateway-authorizer": { - "providerARNs": [ - { - "Fn::GetAtt": [ - "MyUserPool", - "Arn" - ] - } - ], - "type": "cognito_user_pools" - }, - "x-amazon-apigateway-authtype": "cognito_user_pools" - } + "SourceArn": { + "Fn::Sub": [ + "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${__ApiId__}/authorizers/*", + { + "__ApiId__": { + "Ref": "MyApiWithLambdaRequestAuth" + } + } + ] } } - } - }, - "MyApiWithLambdaRequestAuthMyLambdaRequestAuthAuthorizerPermission": { - "Type": "AWS::Lambda::Permission", - "Properties": { - "Action": "lambda:InvokeFunction", - "Principal": "apigateway.amazonaws.com", - "FunctionName": { - "Fn::GetAtt": [ - "MyAuthFn", - "Arn" - ] - }, - "SourceArn": { - "Fn::Sub": [ - "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${__ApiId__}/authorizers/*", - { - "__ApiId__": { - "Ref": "MyApiWithLambdaRequestAuth" + }, + "MyApiWithLambdaRequestAuthProdStage": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "DeploymentId": { + "Ref": "MyApiWithLambdaRequestAuthDeploymentfeb40d0e71" + }, + "RestApiId": { + "Ref": "MyApiWithLambdaRequestAuth" + }, + "StageName": "Prod" + } + }, + "MyFnLambdaTokenPermissionProd": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "Principal": "apigateway.amazonaws.com", + "FunctionName": { + "Ref": "MyFn" + }, + "SourceArn": { + "Fn::Sub": [ + "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${__ApiId__}/${__Stage__}/GET/lambda-token", + { + "__Stage__": "*", + "__ApiId__": { + "Ref": "MyApiWithLambdaTokenAuth" + } } - } - ] + ] + } } - } - }, - "MyApiWithLambdaRequestAuthProdStage": { - "Type": "AWS::ApiGateway::Stage", - "Properties": { - "DeploymentId": { - "Ref": "MyApiWithLambdaRequestAuthDeploymentfeb40d0e71" - }, - "RestApiId": { - "Ref": "MyApiWithLambdaRequestAuth" - }, - "StageName": "Prod" - } - }, - "MyFnLambdaTokenPermissionProd": { - "Type": "AWS::Lambda::Permission", - "Properties": { - "Action": "lambda:InvokeFunction", - "Principal": "apigateway.amazonaws.com", - "FunctionName": { - "Ref": "MyFn" - }, - "SourceArn": { - "Fn::Sub": [ - "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${__ApiId__}/${__Stage__}/GET/lambda-token", - { - "__Stage__": "*", - "__ApiId__": { - "Ref": "MyApiWithLambdaTokenAuth" + }, + "MyApiWithLambdaTokenAuthMyLambdaTokenAuthAuthorizerPermission": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "Principal": "apigateway.amazonaws.com", + "FunctionName": { + "Fn::GetAtt": [ + "MyAuthFn", + "Arn" + ] + }, + "SourceArn": { + "Fn::Sub": [ + "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${__ApiId__}/authorizers/*", + { + "__ApiId__": { + "Ref": "MyApiWithLambdaTokenAuth" + } + } + ] + } + } + }, + "MyFnLambdaRequestPermissionProd": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "Principal": "apigateway.amazonaws.com", + "FunctionName": { + "Ref": "MyFn" + }, + "SourceArn": { + "Fn::Sub": [ + "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${__ApiId__}/${__Stage__}/GET/lambda-request", + { + "__Stage__": "*", + "__ApiId__": { + "Ref": "MyApiWithLambdaRequestAuth" + } + } + ] + } + } + }, + "MyApiWithLambdaTokenAuth": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "Body": { + "info": { + "version": "1.0", + "title": { + "Ref": "AWS::StackName" + } + }, + "paths": { + "/lambda-token": { + "get": { + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "type": "aws_proxy", + "uri": { + "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${MyFn.Arn}/invocations" + } + }, + "security": [ + { + "NONE": [] + } + ], + "responses": {} + } + } + }, + "swagger": "2.0", + "securityDefinitions": { + "MyLambdaTokenAuth": { + "in": "header", + "type": "apiKey", + "name": "Authorization", + "x-amazon-apigateway-authorizer": { + "type": "token", + "authorizerUri": { + "Fn::Sub": [ + "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${__FunctionArn__}/invocations", + { + "__FunctionArn__": { + "Fn::GetAtt": [ + "MyAuthFn", + "Arn" + ] + } + } + ] + } + }, + "x-amazon-apigateway-authtype": "custom" } } - ] + } } - } - }, - "MyApiWithLambdaTokenAuthMyLambdaTokenAuthAuthorizerPermission": { - "Type": "AWS::Lambda::Permission", - "Properties": { - "Action": "lambda:InvokeFunction", - "Principal": "apigateway.amazonaws.com", - "FunctionName": { - "Fn::GetAtt": [ - "MyAuthFn", - "Arn" - ] - }, - "SourceArn": { - "Fn::Sub": [ - "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${__ApiId__}/authorizers/*", + }, + "MyApiWithLambdaTokenAuthProdStage": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "DeploymentId": { + "Ref": "MyApiWithLambdaTokenAuthDeployment4644d735d8" + }, + "RestApiId": { + "Ref": "MyApiWithLambdaTokenAuth" + }, + "StageName": "Prod" + } + }, + "MyApiWithLambdaRequestAuthDeploymentfeb40d0e71": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "RestApiId": { + "Ref": "MyApiWithLambdaRequestAuth" + }, + "Description": "RestApi deployment id: feb40d0e712dce07ba2392d6bb86eff0c5b22b7b", + "StageName": "Stage" + } + }, + "MyUserPool": { + "Type": "AWS::Cognito::UserPool", + "Properties": { + "UsernameAttributes": [ + "email" + ], + "UserPoolName": "UserPoolName", + "Policies": { + "PasswordPolicy": { + "MinimumLength": 8 + } + }, + "Schema": [ { - "__ApiId__": { - "Ref": "MyApiWithLambdaTokenAuth" - } + "AttributeDataType": "String", + "Required": false, + "Name": "email" } ] } - } - }, - "MyFnLambdaRequestPermissionProd": { - "Type": "AWS::Lambda::Permission", - "Properties": { - "Action": "lambda:InvokeFunction", - "Principal": "apigateway.amazonaws.com", - "FunctionName": { - "Ref": "MyFn" - }, - "SourceArn": { - "Fn::Sub": [ - "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${__ApiId__}/${__Stage__}/GET/lambda-request", + }, + "MyAuthFn": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Handler": "index.handler", + "Code": { + "ZipFile": "exports.handler = async (event) => {\n return {\n statusCode: 200,\n body: JSON.stringify(event),\n headers: {}\n }\n}\n" + }, + "Role": { + "Fn::GetAtt": [ + "MyAuthFnRole", + "Arn" + ] + }, + "Runtime": "nodejs12.x", + "Tags": [ { - "__Stage__": "*", - "__ApiId__": { - "Ref": "MyApiWithLambdaRequestAuth" - } + "Value": "SAM", + "Key": "lambda:createdBy" } ] } - } - }, - "MyApiWithLambdaTokenAuth": { - "Type": "AWS::ApiGateway::RestApi", - "Properties": { - "Body": { - "info": { - "version": "1.0", - "title": { - "Ref": "AWS::StackName" - } - }, - "paths": { - "/lambda-token": { - "get": { - "x-amazon-apigateway-integration": { - "httpMethod": "POST", - "type": "aws_proxy", - "uri": { - "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${MyFn.Arn}/invocations" - } - }, - "security": [ - { - "NONE": [] - } - ], - "responses": {} + }, + "MyApiWithLambdaRequestAuth": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "Body": { + "info": { + "version": "1.0", + "title": { + "Ref": "AWS::StackName" } - } - }, - "swagger": "2.0", - "securityDefinitions": { - "MyLambdaTokenAuth": { - "in": "header", - "type": "apiKey", - "name": "Authorization", - "x-amazon-apigateway-authorizer": { - "type": "token", - "authorizerUri": { - "Fn::Sub": [ - "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${__FunctionArn__}/invocations", + }, + "paths": { + "/lambda-request": { + "get": { + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "passthroughBehavior": "when_no_match", + "type": "aws_proxy", + "uri": { + "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${MyFn.Arn}/invocations" + } + }, + "security": [ { - "__FunctionArn__": { - "Fn::GetAtt": [ - "MyAuthFn", - "Arn" - ] - } + "NONE": [] } - ] + ], + "responses": {} } - }, - "x-amazon-apigateway-authtype": "custom" - } - } - } - } - }, - "MyApiWithLambdaTokenAuthProdStage": { - "Type": "AWS::ApiGateway::Stage", - "Properties": { - "DeploymentId": { - "Ref": "MyApiWithLambdaTokenAuthDeployment4644d735d8" - }, - "RestApiId": { - "Ref": "MyApiWithLambdaTokenAuth" - }, - "StageName": "Prod" - } - }, - "MyApiWithLambdaRequestAuthDeploymentfeb40d0e71": { - "Type": "AWS::ApiGateway::Deployment", - "Properties": { - "RestApiId": { - "Ref": "MyApiWithLambdaRequestAuth" - }, - "Description": "RestApi deployment id: feb40d0e712dce07ba2392d6bb86eff0c5b22b7b", - "StageName": "Stage" - } - }, - "MyUserPool": { - "Type": "AWS::Cognito::UserPool", - "Properties": { - "UsernameAttributes": [ - "email" - ], - "UserPoolName": "UserPoolName", - "Policies": { - "PasswordPolicy": { - "MinimumLength": 8 - } - }, - "Schema": [ - { - "AttributeDataType": "String", - "Required": false, - "Name": "email" - } - ] - } - }, - "MyAuthFn": { - "Type": "AWS::Lambda::Function", - "Properties": { - "Handler": "index.handler", - "Code": { - "ZipFile": "exports.handler = async (event) => {\n return {\n statusCode: 200,\n body: JSON.stringify(event),\n headers: {}\n }\n}\n" - }, - "Role": { - "Fn::GetAtt": [ - "MyAuthFnRole", - "Arn" - ] - }, - "Runtime": "nodejs12.x", - "Tags": [ - { - "Value": "SAM", - "Key": "lambda:createdBy" - } - ] - } - }, - "MyApiWithLambdaRequestAuth": { - "Type": "AWS::ApiGateway::RestApi", - "Properties": { - "Body": { - "info": { - "version": "1.0", - "title": { - "Ref": "AWS::StackName" - } - }, - "paths": { - "/lambda-request": { - "get": { - "x-amazon-apigateway-integration": { - "httpMethod": "POST", - "passthroughBehavior": "when_no_match", - "type": "aws_proxy", - "uri": { - "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${MyFn.Arn}/invocations" + } + }, + "swagger": 2.0, + "schemes": [ + "https" + ], + "securityDefinitions": { + "MyLambdaRequestAuth": { + "in": "header", + "type": "apiKey", + "name": "Unused", + "x-amazon-apigateway-authorizer": { + "type": "request", + "identitySource": "method.request.header.Authorization1", + "authorizerUri": { + "Fn::Sub": [ + "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${__FunctionArn__}/invocations", + { + "__FunctionArn__": { + "Fn::GetAtt": [ + "MyAuthFn", + "Arn" + ] + } + } + ] } }, - "security": [ - { - "NONE": [] - } - ], - "responses": {} + "x-amazon-apigateway-authtype": "custom" } } + } + } + }, + "MyApiWithLambdaTokenAuthDeployment4644d735d8": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "RestApiId": { + "Ref": "MyApiWithLambdaTokenAuth" }, - "swagger": 2.0, - "schemes": [ - "https" + "Description": "RestApi deployment id: 4644d735d869a70806f7145ca725b1c8cb248fb7", + "StageName": "Stage" + } + }, + "MyAuthFnRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "ManagedPolicyArns": [ + "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" ], - "securityDefinitions": { - "MyLambdaRequestAuth": { - "in": "header", - "type": "apiKey", - "name": "Unused", - "x-amazon-apigateway-authorizer": { - "type": "request", - "identitySource": "method.request.header.Authorization1", - "authorizerUri": { - "Fn::Sub": [ - "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${__FunctionArn__}/invocations", - { - "__FunctionArn__": { - "Fn::GetAtt": [ - "MyAuthFn", - "Arn" - ] - } - } + "Tags": [ + { + "Value": "SAM", + "Key": "lambda:createdBy" + } + ], + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" ] } - }, - "x-amazon-apigateway-authtype": "custom" - } + } + ] } } - } - }, - "MyApiWithLambdaTokenAuthDeployment4644d735d8": { - "Type": "AWS::ApiGateway::Deployment", - "Properties": { - "RestApiId": { - "Ref": "MyApiWithLambdaTokenAuth" - }, - "Description": "RestApi deployment id: 4644d735d869a70806f7145ca725b1c8cb248fb7", - "StageName": "Stage" - } - }, - "MyAuthFnRole": { - "Type": "AWS::IAM::Role", - "Properties": { - "ManagedPolicyArns": [ - "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" - ], - "Tags": [ - { - "Value": "SAM", - "Key": "lambda:createdBy" - } - ], - "AssumeRolePolicyDocument": { - "Version": "2012-10-17", - "Statement": [ + }, + "MyApiWithCognitoAuthDeploymentf67b169f98": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "RestApiId": { + "Ref": "MyApiWithCognitoAuth" + }, + "Description": "RestApi deployment id: f67b169f98fefb4627c6065af2d5e26ca6ea4da8", + "StageName": "Stage" + } + }, + "MyFnRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "ManagedPolicyArns": [ + "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "Tags": [ { - "Action": [ - "sts:AssumeRole" - ], - "Effect": "Allow", - "Principal": { - "Service": [ - "lambda.amazonaws.com" - ] - } + "Value": "SAM", + "Key": "lambda:createdBy" } - ] - } - } - }, - "MyApiWithCognitoAuthDeploymentf67b169f98": { - "Type": "AWS::ApiGateway::Deployment", - "Properties": { - "RestApiId": { - "Ref": "MyApiWithCognitoAuth" - }, - "Description": "RestApi deployment id: f67b169f98fefb4627c6065af2d5e26ca6ea4da8", - "StageName": "Stage" - } - }, - "MyFnRole": { - "Type": "AWS::IAM::Role", - "Properties": { - "ManagedPolicyArns": [ - "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" - ], - "Tags": [ - { - "Value": "SAM", - "Key": "lambda:createdBy" + ], + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] } - ], - "AssumeRolePolicyDocument": { - "Version": "2012-10-17", - "Statement": [ + } + }, + "MyApiWithCognitoAuthProdStage": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "DeploymentId": { + "Ref": "MyApiWithCognitoAuthDeploymentf67b169f98" + }, + "RestApiId": { + "Ref": "MyApiWithCognitoAuth" + }, + "StageName": "Prod" + } + }, + "MyFn": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Handler": "index.handler", + "Code": { + "ZipFile": "exports.handler = async (event) => {\n return {\n statusCode: 200,\n body: JSON.stringify(event),\n headers: {}\n }\n}\n" + }, + "Role": { + "Fn::GetAtt": [ + "MyFnRole", + "Arn" + ] + }, + "Runtime": "nodejs12.x", + "Tags": [ { - "Action": [ - "sts:AssumeRole" - ], - "Effect": "Allow", - "Principal": { - "Service": [ - "lambda.amazonaws.com" - ] - } + "Value": "SAM", + "Key": "lambda:createdBy" } ] } } - }, - "MyApiWithCognitoAuthProdStage": { - "Type": "AWS::ApiGateway::Stage", - "Properties": { - "DeploymentId": { - "Ref": "MyApiWithCognitoAuthDeploymentf67b169f98" - }, - "RestApiId": { - "Ref": "MyApiWithCognitoAuth" - }, - "StageName": "Prod" - } - }, - "MyFn": { - "Type": "AWS::Lambda::Function", - "Properties": { - "Handler": "index.handler", - "Code": { - "ZipFile": "exports.handler = async (event) => {\n return {\n statusCode: 200,\n body: JSON.stringify(event),\n headers: {}\n }\n}\n" - }, - "Role": { - "Fn::GetAtt": [ - "MyFnRole", - "Arn" - ] - }, - "Runtime": "nodejs12.x", - "Tags": [ - { - "Value": "SAM", - "Key": "lambda:createdBy" - } - ] - } } - } -} \ No newline at end of file + } \ No newline at end of file diff --git a/tests/translator/output/api_with_usageplans_intrinsics.json b/tests/translator/output/api_with_usageplans_intrinsics.json index 3c43187e7..ce2e5413d 100644 --- a/tests/translator/output/api_with_usageplans_intrinsics.json +++ b/tests/translator/output/api_with_usageplans_intrinsics.json @@ -68,7 +68,8 @@ }, "DependsOn": [ "MyApiOne" - ] + ], + "Condition": "C1" }, "MyApiTwoUsagePlan": { "Type": "AWS::ApiGateway::UsagePlan", @@ -140,7 +141,8 @@ }, "DependsOn": [ "MyApiOneUsagePlan" - ] + ], + "Condition": "C1" }, "MyFunctionTwoRole": { "Type": "AWS::IAM::Role", @@ -185,7 +187,8 @@ }, "DependsOn": [ "MyApiOneApiKey" - ] + ], + "Condition": "C1" }, "MyApiTwoApiKey": { "Type": "AWS::ApiGateway::ApiKey", diff --git a/tests/translator/output/aws-cn/api_with_swagger_authorizer_none.json b/tests/translator/output/aws-cn/api_with_swagger_authorizer_none.json index bf67b0a5a..8ed6a1d1d 100644 --- a/tests/translator/output/aws-cn/api_with_swagger_authorizer_none.json +++ b/tests/translator/output/aws-cn/api_with_swagger_authorizer_none.json @@ -1,499 +1,499 @@ { - "Resources": { - "MyFnCognitoPermissionProd": { - "Type": "AWS::Lambda::Permission", - "Properties": { - "Action": "lambda:InvokeFunction", - "Principal": "apigateway.amazonaws.com", - "FunctionName": { - "Ref": "MyFn" - }, - "SourceArn": { - "Fn::Sub": [ - "arn:aws-cn:execute-api:${AWS::Region}:${AWS::AccountId}:${__ApiId__}/${__Stage__}/GET/cognito", - { - "__Stage__": "*", - "__ApiId__": { - "Ref": "MyApiWithCognitoAuth" + "Resources": { + "MyFnCognitoPermissionProd": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "Principal": "apigateway.amazonaws.com", + "FunctionName": { + "Ref": "MyFn" + }, + "SourceArn": { + "Fn::Sub": [ + "arn:aws-cn:execute-api:${AWS::Region}:${AWS::AccountId}:${__ApiId__}/${__Stage__}/GET/cognito", + { + "__Stage__": "*", + "__ApiId__": { + "Ref": "MyApiWithCognitoAuth" + } } - } - ] + ] + } } - } - }, - "MyApiWithCognitoAuth": { - "Type": "AWS::ApiGateway::RestApi", - "Properties": { - "Body": { - "info": { - "version": "1.0", - "title": { - "Ref": "AWS::StackName" - } - }, - "paths": { - "/cognito": { - "get": { - "x-amazon-apigateway-integration": { - "httpMethod": "POST", - "type": "aws_proxy", - "uri": { - "Fn::Sub": "arn:aws-cn:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${MyFn.Arn}/invocations" - } + }, + "MyApiWithCognitoAuth": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "Body": { + "info": { + "version": "1.0", + "title": { + "Ref": "AWS::StackName" + } + }, + "paths": { + "/cognito": { + "get": { + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "type": "aws_proxy", + "uri": { + "Fn::Sub": "arn:aws-cn:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${MyFn.Arn}/invocations" + } + }, + "security": [ + { + "NONE": [] + } + ], + "responses": {} + } + } + }, + "swagger": "2.0", + "securityDefinitions": { + "MyCognitoAuth": { + "in": "header", + "type": "apiKey", + "name": "Authorization", + "x-amazon-apigateway-authorizer": { + "providerARNs": [ + { + "Fn::GetAtt": [ + "MyUserPool", + "Arn" + ] + } + ], + "type": "cognito_user_pools" }, - "security": [ - { - "NONE": [] - } - ], - "responses": {} + "x-amazon-apigateway-authtype": "cognito_user_pools" } } }, - "swagger": "2.0", - "securityDefinitions": { - "MyCognitoAuth": { - "in": "header", - "type": "apiKey", - "name": "Authorization", - "x-amazon-apigateway-authorizer": { - "providerARNs": [ - { - "Fn::GetAtt": [ - "MyUserPool", - "Arn" + "EndpointConfiguration": { + "Types": [ + "REGIONAL" + ] + }, + "Parameters": { + "endpointConfigurationTypes": "REGIONAL" + } + } + }, + "MyApiWithLambdaRequestAuthDeploymentbad519dbd8": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "RestApiId": { + "Ref": "MyApiWithLambdaRequestAuth" + }, + "Description": "RestApi deployment id: bad519dbd801b0e2c63dc6f2011f43bce33c262a", + "StageName": "Stage" + } + }, + "MyApiWithLambdaTokenAuthDeployment29918bbdc1": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "RestApiId": { + "Ref": "MyApiWithLambdaTokenAuth" + }, + "Description": "RestApi deployment id: 29918bbdc180ceedbabcf34c01ca5342e8c019cd", + "StageName": "Stage" + } + }, + "MyFnLambdaTokenPermissionProd": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "Principal": "apigateway.amazonaws.com", + "FunctionName": { + "Ref": "MyFn" + }, + "SourceArn": { + "Fn::Sub": [ + "arn:aws-cn:execute-api:${AWS::Region}:${AWS::AccountId}:${__ApiId__}/${__Stage__}/GET/lambda-token", + { + "__Stage__": "*", + "__ApiId__": { + "Ref": "MyApiWithLambdaTokenAuth" + } + } + ] + } + } + }, + "MyApiWithLambdaTokenAuthMyLambdaTokenAuthAuthorizerPermission": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "Principal": "apigateway.amazonaws.com", + "FunctionName": { + "Fn::GetAtt": [ + "MyAuthFn", + "Arn" + ] + }, + "SourceArn": { + "Fn::Sub": [ + "arn:aws-cn:execute-api:${AWS::Region}:${AWS::AccountId}:${__ApiId__}/authorizers/*", + { + "__ApiId__": { + "Ref": "MyApiWithLambdaTokenAuth" + } + } + ] + } + } + }, + "MyFnLambdaRequestPermissionProd": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "Principal": "apigateway.amazonaws.com", + "FunctionName": { + "Ref": "MyFn" + }, + "SourceArn": { + "Fn::Sub": [ + "arn:aws-cn:execute-api:${AWS::Region}:${AWS::AccountId}:${__ApiId__}/${__Stage__}/GET/lambda-request", + { + "__Stage__": "*", + "__ApiId__": { + "Ref": "MyApiWithLambdaRequestAuth" + } + } + ] + } + } + }, + "MyApiWithLambdaTokenAuth": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "Body": { + "info": { + "version": "1.0", + "title": { + "Ref": "AWS::StackName" + } + }, + "paths": { + "/lambda-token": { + "get": { + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "type": "aws_proxy", + "uri": { + "Fn::Sub": "arn:aws-cn:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${MyFn.Arn}/invocations" + } + }, + "security": [ + { + "NONE": [] + } + ], + "responses": {} + } + } + }, + "swagger": "2.0", + "securityDefinitions": { + "MyLambdaTokenAuth": { + "in": "header", + "type": "apiKey", + "name": "Authorization", + "x-amazon-apigateway-authorizer": { + "type": "token", + "authorizerUri": { + "Fn::Sub": [ + "arn:aws-cn:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${__FunctionArn__}/invocations", + { + "__FunctionArn__": { + "Fn::GetAtt": [ + "MyAuthFn", + "Arn" + ] + } + } ] } - ], - "type": "cognito_user_pools" - }, - "x-amazon-apigateway-authtype": "cognito_user_pools" + }, + "x-amazon-apigateway-authtype": "custom" + } } + }, + "EndpointConfiguration": { + "Types": [ + "REGIONAL" + ] + }, + "Parameters": { + "endpointConfigurationTypes": "REGIONAL" } - }, - "EndpointConfiguration": { - "Types": [ - "REGIONAL" - ] - }, - "Parameters": { - "endpointConfigurationTypes": "REGIONAL" } - } - }, - "MyApiWithLambdaRequestAuthDeploymentbad519dbd8": { - "Type": "AWS::ApiGateway::Deployment", - "Properties": { - "RestApiId": { - "Ref": "MyApiWithLambdaRequestAuth" - }, - "Description": "RestApi deployment id: bad519dbd801b0e2c63dc6f2011f43bce33c262a", - "StageName": "Stage" - } - }, - "MyApiWithLambdaTokenAuthDeployment29918bbdc1": { - "Type": "AWS::ApiGateway::Deployment", - "Properties": { - "RestApiId": { - "Ref": "MyApiWithLambdaTokenAuth" - }, - "Description": "RestApi deployment id: 29918bbdc180ceedbabcf34c01ca5342e8c019cd", - "StageName": "Stage" - } - }, - "MyFnLambdaTokenPermissionProd": { - "Type": "AWS::Lambda::Permission", - "Properties": { - "Action": "lambda:InvokeFunction", - "Principal": "apigateway.amazonaws.com", - "FunctionName": { - "Ref": "MyFn" - }, - "SourceArn": { - "Fn::Sub": [ - "arn:aws-cn:execute-api:${AWS::Region}:${AWS::AccountId}:${__ApiId__}/${__Stage__}/GET/lambda-token", - { - "__Stage__": "*", - "__ApiId__": { - "Ref": "MyApiWithLambdaTokenAuth" - } - } - ] + }, + "MyApiWithLambdaTokenAuthProdStage": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "DeploymentId": { + "Ref": "MyApiWithLambdaTokenAuthDeployment29918bbdc1" + }, + "RestApiId": { + "Ref": "MyApiWithLambdaTokenAuth" + }, + "StageName": "Prod" } - } - }, - "MyApiWithLambdaTokenAuthMyLambdaTokenAuthAuthorizerPermission": { - "Type": "AWS::Lambda::Permission", - "Properties": { - "Action": "lambda:InvokeFunction", - "Principal": "apigateway.amazonaws.com", - "FunctionName": { - "Fn::GetAtt": [ - "MyAuthFn", - "Arn" - ] - }, - "SourceArn": { - "Fn::Sub": [ - "arn:aws-cn:execute-api:${AWS::Region}:${AWS::AccountId}:${__ApiId__}/authorizers/*", + }, + "MyApiWithLambdaRequestAuthProdStage": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "DeploymentId": { + "Ref": "MyApiWithLambdaRequestAuthDeploymentbad519dbd8" + }, + "RestApiId": { + "Ref": "MyApiWithLambdaRequestAuth" + }, + "StageName": "Prod" + } + }, + "MyUserPool": { + "Type": "AWS::Cognito::UserPool", + "Properties": { + "UsernameAttributes": [ + "email" + ], + "UserPoolName": "UserPoolName", + "Policies": { + "PasswordPolicy": { + "MinimumLength": 8 + } + }, + "Schema": [ { - "__ApiId__": { - "Ref": "MyApiWithLambdaTokenAuth" - } + "AttributeDataType": "String", + "Required": false, + "Name": "email" } ] } - } - }, - "MyFnLambdaRequestPermissionProd": { - "Type": "AWS::Lambda::Permission", - "Properties": { - "Action": "lambda:InvokeFunction", - "Principal": "apigateway.amazonaws.com", - "FunctionName": { - "Ref": "MyFn" - }, - "SourceArn": { - "Fn::Sub": [ - "arn:aws-cn:execute-api:${AWS::Region}:${AWS::AccountId}:${__ApiId__}/${__Stage__}/GET/lambda-request", + }, + "MyAuthFn": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Handler": "index.handler", + "Code": { + "ZipFile": "exports.handler = async (event) => {\n return {\n statusCode: 200,\n body: JSON.stringify(event),\n headers: {}\n }\n}\n" + }, + "Role": { + "Fn::GetAtt": [ + "MyAuthFnRole", + "Arn" + ] + }, + "Runtime": "nodejs12.x", + "Tags": [ { - "__Stage__": "*", - "__ApiId__": { - "Ref": "MyApiWithLambdaRequestAuth" - } + "Value": "SAM", + "Key": "lambda:createdBy" } ] } - } - }, - "MyApiWithLambdaTokenAuth": { - "Type": "AWS::ApiGateway::RestApi", - "Properties": { - "Body": { - "info": { - "version": "1.0", - "title": { - "Ref": "AWS::StackName" - } - }, - "paths": { - "/lambda-token": { - "get": { - "x-amazon-apigateway-integration": { - "httpMethod": "POST", - "type": "aws_proxy", - "uri": { - "Fn::Sub": "arn:aws-cn:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${MyFn.Arn}/invocations" - } - }, - "security": [ - { - "NONE": [] - } - ], - "responses": {} + }, + "MyApiWithLambdaRequestAuth": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "Body": { + "info": { + "version": "1.0", + "title": { + "Ref": "AWS::StackName" } - } - }, - "swagger": "2.0", - "securityDefinitions": { - "MyLambdaTokenAuth": { - "in": "header", - "type": "apiKey", - "name": "Authorization", - "x-amazon-apigateway-authorizer": { - "type": "token", - "authorizerUri": { - "Fn::Sub": [ - "arn:aws-cn:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${__FunctionArn__}/invocations", + }, + "paths": { + "/lambda-request": { + "get": { + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "passthroughBehavior": "when_no_match", + "type": "aws_proxy", + "uri": { + "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${MyFn.Arn}/invocations" + } + }, + "security": [ { - "__FunctionArn__": { - "Fn::GetAtt": [ - "MyAuthFn", - "Arn" - ] - } + "NONE": [] } - ] + ], + "responses": {} } - }, - "x-amazon-apigateway-authtype": "custom" - } - } - }, - "EndpointConfiguration": { - "Types": [ - "REGIONAL" - ] - }, - "Parameters": { - "endpointConfigurationTypes": "REGIONAL" - } - } - }, - "MyApiWithLambdaTokenAuthProdStage": { - "Type": "AWS::ApiGateway::Stage", - "Properties": { - "DeploymentId": { - "Ref": "MyApiWithLambdaTokenAuthDeployment29918bbdc1" - }, - "RestApiId": { - "Ref": "MyApiWithLambdaTokenAuth" - }, - "StageName": "Prod" - } - }, - "MyApiWithLambdaRequestAuthProdStage": { - "Type": "AWS::ApiGateway::Stage", - "Properties": { - "DeploymentId": { - "Ref": "MyApiWithLambdaRequestAuthDeploymentbad519dbd8" - }, - "RestApiId": { - "Ref": "MyApiWithLambdaRequestAuth" - }, - "StageName": "Prod" - } - }, - "MyUserPool": { - "Type": "AWS::Cognito::UserPool", - "Properties": { - "UsernameAttributes": [ - "email" - ], - "UserPoolName": "UserPoolName", - "Policies": { - "PasswordPolicy": { - "MinimumLength": 8 - } - }, - "Schema": [ - { - "AttributeDataType": "String", - "Required": false, - "Name": "email" - } - ] - } - }, - "MyAuthFn": { - "Type": "AWS::Lambda::Function", - "Properties": { - "Handler": "index.handler", - "Code": { - "ZipFile": "exports.handler = async (event) => {\n return {\n statusCode: 200,\n body: JSON.stringify(event),\n headers: {}\n }\n}\n" - }, - "Role": { - "Fn::GetAtt": [ - "MyAuthFnRole", - "Arn" - ] - }, - "Runtime": "nodejs12.x", - "Tags": [ - { - "Value": "SAM", - "Key": "lambda:createdBy" - } - ] - } - }, - "MyApiWithLambdaRequestAuth": { - "Type": "AWS::ApiGateway::RestApi", - "Properties": { - "Body": { - "info": { - "version": "1.0", - "title": { - "Ref": "AWS::StackName" - } - }, - "paths": { - "/lambda-request": { - "get": { - "x-amazon-apigateway-integration": { - "httpMethod": "POST", - "passthroughBehavior": "when_no_match", - "type": "aws_proxy", - "uri": { - "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${MyFn.Arn}/invocations" + } + }, + "swagger": 2.0, + "schemes": [ + "https" + ], + "securityDefinitions": { + "MyLambdaRequestAuth": { + "in": "header", + "type": "apiKey", + "name": "Unused", + "x-amazon-apigateway-authorizer": { + "type": "request", + "identitySource": "method.request.header.Authorization1", + "authorizerUri": { + "Fn::Sub": [ + "arn:aws-cn:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${__FunctionArn__}/invocations", + { + "__FunctionArn__": { + "Fn::GetAtt": [ + "MyAuthFn", + "Arn" + ] + } + } + ] } }, - "security": [ - { - "NONE": [] - } - ], - "responses": {} + "x-amazon-apigateway-authtype": "custom" } } }, - "swagger": 2.0, - "schemes": [ - "https" + "EndpointConfiguration": { + "Types": [ + "REGIONAL" + ] + }, + "Parameters": { + "endpointConfigurationTypes": "REGIONAL" + } + } + }, + "MyApiWithCognitoAuthDeployment77726cd3cb": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "RestApiId": { + "Ref": "MyApiWithCognitoAuth" + }, + "Description": "RestApi deployment id: 77726cd3cb8eddd94a4856ca8d65ee0f39d03b2e", + "StageName": "Stage" + } + }, + "MyAuthFnRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "ManagedPolicyArns": [ + "arn:aws-cn:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" ], - "securityDefinitions": { - "MyLambdaRequestAuth": { - "in": "header", - "type": "apiKey", - "name": "Unused", - "x-amazon-apigateway-authorizer": { - "type": "request", - "identitySource": "method.request.header.Authorization1", - "authorizerUri": { - "Fn::Sub": [ - "arn:aws-cn:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${__FunctionArn__}/invocations", - { - "__FunctionArn__": { - "Fn::GetAtt": [ - "MyAuthFn", - "Arn" - ] - } - } + "Tags": [ + { + "Value": "SAM", + "Key": "lambda:createdBy" + } + ], + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" ] } - }, - "x-amazon-apigateway-authtype": "custom" - } + } + ] } - }, - "EndpointConfiguration": { - "Types": [ - "REGIONAL" - ] - }, - "Parameters": { - "endpointConfigurationTypes": "REGIONAL" } - } - }, - "MyApiWithCognitoAuthDeployment77726cd3cb": { - "Type": "AWS::ApiGateway::Deployment", - "Properties": { - "RestApiId": { - "Ref": "MyApiWithCognitoAuth" - }, - "Description": "RestApi deployment id: 77726cd3cb8eddd94a4856ca8d65ee0f39d03b2e", - "StageName": "Stage" - } - }, - "MyAuthFnRole": { - "Type": "AWS::IAM::Role", - "Properties": { - "ManagedPolicyArns": [ - "arn:aws-cn:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" - ], - "Tags": [ - { - "Value": "SAM", - "Key": "lambda:createdBy" - } - ], - "AssumeRolePolicyDocument": { - "Version": "2012-10-17", - "Statement": [ + }, + "MyFnRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "ManagedPolicyArns": [ + "arn:aws-cn:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "Tags": [ { - "Action": [ - "sts:AssumeRole" - ], - "Effect": "Allow", - "Principal": { - "Service": [ - "lambda.amazonaws.com" - ] - } + "Value": "SAM", + "Key": "lambda:createdBy" } - ] - } - } - }, - "MyFnRole": { - "Type": "AWS::IAM::Role", - "Properties": { - "ManagedPolicyArns": [ - "arn:aws-cn:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" - ], - "Tags": [ - { - "Value": "SAM", - "Key": "lambda:createdBy" + ], + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] } - ], - "AssumeRolePolicyDocument": { - "Version": "2012-10-17", - "Statement": [ + } + }, + "MyApiWithCognitoAuthProdStage": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "DeploymentId": { + "Ref": "MyApiWithCognitoAuthDeployment77726cd3cb" + }, + "RestApiId": { + "Ref": "MyApiWithCognitoAuth" + }, + "StageName": "Prod" + } + }, + "MyFn": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Handler": "index.handler", + "Code": { + "ZipFile": "exports.handler = async (event) => {\n return {\n statusCode: 200,\n body: JSON.stringify(event),\n headers: {}\n }\n}\n" + }, + "Role": { + "Fn::GetAtt": [ + "MyFnRole", + "Arn" + ] + }, + "Runtime": "nodejs12.x", + "Tags": [ { - "Action": [ - "sts:AssumeRole" - ], - "Effect": "Allow", - "Principal": { - "Service": [ - "lambda.amazonaws.com" - ] - } + "Value": "SAM", + "Key": "lambda:createdBy" } ] } - } - }, - "MyApiWithCognitoAuthProdStage": { - "Type": "AWS::ApiGateway::Stage", - "Properties": { - "DeploymentId": { - "Ref": "MyApiWithCognitoAuthDeployment77726cd3cb" - }, - "RestApiId": { - "Ref": "MyApiWithCognitoAuth" - }, - "StageName": "Prod" - } - }, - "MyFn": { - "Type": "AWS::Lambda::Function", - "Properties": { - "Handler": "index.handler", - "Code": { - "ZipFile": "exports.handler = async (event) => {\n return {\n statusCode: 200,\n body: JSON.stringify(event),\n headers: {}\n }\n}\n" - }, - "Role": { - "Fn::GetAtt": [ - "MyFnRole", - "Arn" - ] - }, - "Runtime": "nodejs12.x", - "Tags": [ - { - "Value": "SAM", - "Key": "lambda:createdBy" - } - ] - } - }, - "MyApiWithLambdaRequestAuthMyLambdaRequestAuthAuthorizerPermission": { - "Type": "AWS::Lambda::Permission", - "Properties": { - "Action": "lambda:InvokeFunction", - "Principal": "apigateway.amazonaws.com", - "FunctionName": { - "Fn::GetAtt": [ - "MyAuthFn", - "Arn" - ] - }, - "SourceArn": { - "Fn::Sub": [ - "arn:aws-cn:execute-api:${AWS::Region}:${AWS::AccountId}:${__ApiId__}/authorizers/*", - { - "__ApiId__": { - "Ref": "MyApiWithLambdaRequestAuth" + }, + "MyApiWithLambdaRequestAuthMyLambdaRequestAuthAuthorizerPermission": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "Principal": "apigateway.amazonaws.com", + "FunctionName": { + "Fn::GetAtt": [ + "MyAuthFn", + "Arn" + ] + }, + "SourceArn": { + "Fn::Sub": [ + "arn:aws-cn:execute-api:${AWS::Region}:${AWS::AccountId}:${__ApiId__}/authorizers/*", + { + "__ApiId__": { + "Ref": "MyApiWithLambdaRequestAuth" + } } - } - ] + ] + } } } } - } -} \ No newline at end of file + } \ No newline at end of file diff --git a/tests/translator/output/aws-cn/api_with_usageplans_intrinsics.json b/tests/translator/output/aws-cn/api_with_usageplans_intrinsics.json index a4d484013..c9a6c4230 100644 --- a/tests/translator/output/aws-cn/api_with_usageplans_intrinsics.json +++ b/tests/translator/output/aws-cn/api_with_usageplans_intrinsics.json @@ -59,7 +59,8 @@ }, "DependsOn": [ "MyApiOne" - ] + ], + "Condition": "C1" }, "MyApiTwoUsagePlan": { "Type": "AWS::ApiGateway::UsagePlan", @@ -101,7 +102,8 @@ }, "DependsOn": [ "MyApiOneApiKey" - ] + ], + "Condition": "C1" }, "MyApiTwoProdStage": { "Type": "AWS::ApiGateway::Stage", @@ -145,7 +147,8 @@ }, "DependsOn": [ "MyApiOneUsagePlan" - ] + ], + "Condition": "C1" }, "MyFunctionTwoRole": { "Type": "AWS::IAM::Role", diff --git a/tests/translator/output/aws-cn/function_event_conditions.json b/tests/translator/output/aws-cn/function_event_conditions.json index 274bbc538..1e1ee3b38 100644 --- a/tests/translator/output/aws-cn/function_event_conditions.json +++ b/tests/translator/output/aws-cn/function_event_conditions.json @@ -529,6 +529,7 @@ "Condition": "MyCondition" }, "MyAwesomeFunctionAnotherSNSWithSQSSubscriptionQueue": { + "Condition": "MyCondition", "Type": "AWS::SQS::Queue", "Properties": {} }, diff --git a/tests/translator/output/aws-cn/implicit_api_deletion_policy_precedence.json b/tests/translator/output/aws-cn/implicit_api_deletion_policy_precedence.json new file mode 100644 index 000000000..33a3bb123 --- /dev/null +++ b/tests/translator/output/aws-cn/implicit_api_deletion_policy_precedence.json @@ -0,0 +1,242 @@ +{ + "Resources": { + "RestApiFunction": { + "Type": "AWS::Lambda::Function", + "DeletionPolicy": "Delete", + "UpdateReplacePolicy": "Retain", + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "todo_list.zip" + }, + "Handler": "index.restapi", + "Role": { + "Fn::GetAtt": [ + "RestApiFunctionRole", + "Arn" + ] + }, + "Runtime": "nodejs12.x", + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "RestApiFunctionRole": { + "Type": "AWS::IAM::Role", + "DeletionPolicy": "Delete", + "UpdateReplacePolicy": "Retain", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + }, + "ManagedPolicyArns": [ + "arn:aws-cn:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", + "arn:aws-cn:iam::aws:policy/AmazonDynamoDBFullAccess" + ], + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "RestApiFunctionGetHtmlPermissionProd": { + "Type": "AWS::Lambda::Permission", + "DeletionPolicy": "Delete", + "UpdateReplacePolicy": "Retain", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Ref": "RestApiFunction" + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Sub": [ + "arn:aws-cn:execute-api:${AWS::Region}:${AWS::AccountId}:${__ApiId__}/${__Stage__}/*/*", + { + "__ApiId__": { + "Ref": "ServerlessRestApi" + }, + "__Stage__": "*" + } + ] + } + } + }, + "GetHtmlFunction": { + "Type": "AWS::Lambda::Function", + "DeletionPolicy": "Retain", + "UpdateReplacePolicy": "Retain", + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "todo_list.zip" + }, + "Handler": "index.gethtml", + "Role": { + "Fn::GetAtt": [ + "GetHtmlFunctionRole", + "Arn" + ] + }, + "Runtime": "nodejs12.x", + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "GetHtmlFunctionRole": { + "Type": "AWS::IAM::Role", + "DeletionPolicy": "Retain", + "UpdateReplacePolicy": "Retain", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + }, + "ManagedPolicyArns": [ + "arn:aws-cn:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", + "arn:aws-cn:iam::aws:policy/AmazonDynamoDBReadOnlyAccess" + ], + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "GetHtmlFunctionGetHtmlPermissionProd": { + "Type": "AWS::Lambda::Permission", + "DeletionPolicy": "Retain", + "UpdateReplacePolicy": "Retain", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Ref": "GetHtmlFunction" + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Sub": [ + "arn:aws-cn:execute-api:${AWS::Region}:${AWS::AccountId}:${__ApiId__}/${__Stage__}/*/{proxy++}", + { + "__ApiId__": { + "Ref": "ServerlessRestApi" + }, + "__Stage__": "*" + } + ] + } + } + }, + "ServerlessRestApi": { + "Type": "AWS::ApiGateway::RestApi", + "DeletionPolicy": "Retain", + "UpdateReplacePolicy": "Retain", + "Properties": { + "Body": { + "swagger": "2.0", + "info": { + "version": "1.0", + "title": { + "Ref": "AWS::StackName" + } + }, + "paths": { + "/{proxy+}": { + "x-amazon-apigateway-any-method": { + "x-amazon-apigateway-integration": { + "type": "aws_proxy", + "httpMethod": "POST", + "uri": { + "Fn::Sub": "arn:aws-cn:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${RestApiFunction.Arn}/invocations" + } + }, + "responses": {} + } + }, + "/{proxy++}": { + "x-amazon-apigateway-any-method": { + "x-amazon-apigateway-integration": { + "type": "aws_proxy", + "httpMethod": "POST", + "uri": { + "Fn::Sub": "arn:aws-cn:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${GetHtmlFunction.Arn}/invocations" + } + }, + "responses": {} + } + } + } + }, + "EndpointConfiguration": { + "Types": [ + "REGIONAL" + ] + }, + "Parameters": { + "endpointConfigurationTypes": "REGIONAL" + } + } + }, + "ServerlessRestApiDeployment1ec0d29ab5": { + "Type": "AWS::ApiGateway::Deployment", + "DeletionPolicy": "Retain", + "UpdateReplacePolicy": "Retain", + "Properties": { + "Description": "RestApi deployment id: 1ec0d29ab5c55018bb989df31615c170b707ae21", + "RestApiId": { + "Ref": "ServerlessRestApi" + }, + "StageName": "Stage" + } + }, + "ServerlessRestApiProdStage": { + "Type": "AWS::ApiGateway::Stage", + "DeletionPolicy": "Retain", + "UpdateReplacePolicy": "Retain", + "Properties": { + "DeploymentId": { + "Ref": "ServerlessRestApiDeployment1ec0d29ab5" + }, + "RestApiId": { + "Ref": "ServerlessRestApi" + }, + "StageName": "Prod" + } + } + } +} \ No newline at end of file diff --git a/tests/translator/output/aws-cn/layer_deletion_policy_precedence.json b/tests/translator/output/aws-cn/layer_deletion_policy_precedence.json new file mode 100644 index 000000000..8bb610ad0 --- /dev/null +++ b/tests/translator/output/aws-cn/layer_deletion_policy_precedence.json @@ -0,0 +1,37 @@ +{ + "Resources": { + "MinimalLayer22b6609c3d": { + "Type": "AWS::Lambda::LayerVersion", + "DeletionPolicy": "Retain", + "Properties": { + "Content": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "layer.zip" + }, + "LayerName": "MinimalLayer" + } + }, + "MinimalLayer2800a44a445": { + "Type": "AWS::Lambda::LayerVersion", + "DeletionPolicy": "Delete", + "Properties": { + "Content": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "layer.zip" + }, + "LayerName": "MinimalLayer2" + } + }, + "MinimalLayer3ac07350a04": { + "Type": "AWS::Lambda::LayerVersion", + "DeletionPolicy": "Retain", + "Properties": { + "Content": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "layer.zip" + }, + "LayerName": "MinimalLayer3" + } + } + } +} \ No newline at end of file diff --git a/tests/translator/output/aws-cn/version_deletion_policy_precedence.json b/tests/translator/output/aws-cn/version_deletion_policy_precedence.json new file mode 100644 index 000000000..1d970410a --- /dev/null +++ b/tests/translator/output/aws-cn/version_deletion_policy_precedence.json @@ -0,0 +1,163 @@ +{ + "Resources": { + "MinimalFunction": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "hello.zip" + }, + "Handler": "hello.handler", + "Role": { + "Fn::GetAtt": [ + "MinimalFunctionRole", + "Arn" + ] + }, + "Runtime": "python2.7", + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "MinimalFunctionVersion640128d35d": { + "Type": "AWS::Lambda::Version", + "DeletionPolicy": "Retain", + "Properties": { + "Description": "sam-testing", + "FunctionName": { + "Ref": "MinimalFunction" + } + } + }, + "MinimalFunctionAliaslive": { + "Type": "AWS::Lambda::Alias", + "Properties": { + "Name": "live", + "FunctionName": { + "Ref": "MinimalFunction" + }, + "FunctionVersion": { + "Fn::GetAtt": [ + "MinimalFunctionVersion640128d35d", + "Version" + ] + } + } + }, + "MinimalFunctionRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + }, + "ManagedPolicyArns": [ + "arn:aws-cn:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "MinimalFunction2": { + "Type": "AWS::Lambda::Function", + "DeletionPolicy": "Delete", + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "hello.zip" + }, + "Handler": "hello.handler", + "Role": { + "Fn::GetAtt": [ + "MinimalFunction2Role", + "Arn" + ] + }, + "Runtime": "python2.7", + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "MinimalFunction2Version640128d35d": { + "Type": "AWS::Lambda::Version", + "DeletionPolicy": "Delete", + "Properties": { + "Description": "sam-testing", + "FunctionName": { + "Ref": "MinimalFunction2" + } + } + }, + "MinimalFunction2Aliaslive": { + "Type": "AWS::Lambda::Alias", + "DeletionPolicy": "Delete", + "Properties": { + "Name": "live", + "FunctionName": { + "Ref": "MinimalFunction2" + }, + "FunctionVersion": { + "Fn::GetAtt": [ + "MinimalFunction2Version640128d35d", + "Version" + ] + } + } + }, + "MinimalFunction2Role": { + "Type": "AWS::IAM::Role", + "DeletionPolicy": "Delete", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + }, + "ManagedPolicyArns": [ + "arn:aws-cn:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + } + } +} \ No newline at end of file diff --git a/tests/translator/output/aws-us-gov/api_with_swagger_authorizer_none.json b/tests/translator/output/aws-us-gov/api_with_swagger_authorizer_none.json index 3edd2c2a6..1fe5153a1 100644 --- a/tests/translator/output/aws-us-gov/api_with_swagger_authorizer_none.json +++ b/tests/translator/output/aws-us-gov/api_with_swagger_authorizer_none.json @@ -1,499 +1,499 @@ { - "Resources": { - "MyFnCognitoPermissionProd": { - "Type": "AWS::Lambda::Permission", - "Properties": { - "Action": "lambda:InvokeFunction", - "Principal": "apigateway.amazonaws.com", - "FunctionName": { - "Ref": "MyFn" - }, - "SourceArn": { - "Fn::Sub": [ - "arn:aws-us-gov:execute-api:${AWS::Region}:${AWS::AccountId}:${__ApiId__}/${__Stage__}/GET/cognito", - { - "__Stage__": "*", - "__ApiId__": { - "Ref": "MyApiWithCognitoAuth" + "Resources": { + "MyFnCognitoPermissionProd": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "Principal": "apigateway.amazonaws.com", + "FunctionName": { + "Ref": "MyFn" + }, + "SourceArn": { + "Fn::Sub": [ + "arn:aws-us-gov:execute-api:${AWS::Region}:${AWS::AccountId}:${__ApiId__}/${__Stage__}/GET/cognito", + { + "__Stage__": "*", + "__ApiId__": { + "Ref": "MyApiWithCognitoAuth" + } } - } - ] + ] + } } - } - }, - "MyApiWithCognitoAuth": { - "Type": "AWS::ApiGateway::RestApi", - "Properties": { - "Body": { - "info": { - "version": "1.0", - "title": { - "Ref": "AWS::StackName" - } - }, - "paths": { - "/cognito": { - "get": { - "x-amazon-apigateway-integration": { - "httpMethod": "POST", - "type": "aws_proxy", - "uri": { - "Fn::Sub": "arn:aws-us-gov:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${MyFn.Arn}/invocations" - } + }, + "MyApiWithCognitoAuth": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "Body": { + "info": { + "version": "1.0", + "title": { + "Ref": "AWS::StackName" + } + }, + "paths": { + "/cognito": { + "get": { + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "type": "aws_proxy", + "uri": { + "Fn::Sub": "arn:aws-us-gov:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${MyFn.Arn}/invocations" + } + }, + "security": [ + { + "NONE": [] + } + ], + "responses": {} + } + } + }, + "swagger": "2.0", + "securityDefinitions": { + "MyCognitoAuth": { + "in": "header", + "type": "apiKey", + "name": "Authorization", + "x-amazon-apigateway-authorizer": { + "providerARNs": [ + { + "Fn::GetAtt": [ + "MyUserPool", + "Arn" + ] + } + ], + "type": "cognito_user_pools" }, - "security": [ - { - "NONE": [] - } - ], - "responses": {} + "x-amazon-apigateway-authtype": "cognito_user_pools" } } }, - "swagger": "2.0", - "securityDefinitions": { - "MyCognitoAuth": { - "in": "header", - "type": "apiKey", - "name": "Authorization", - "x-amazon-apigateway-authorizer": { - "providerARNs": [ - { - "Fn::GetAtt": [ - "MyUserPool", - "Arn" + "EndpointConfiguration": { + "Types": [ + "REGIONAL" + ] + }, + "Parameters": { + "endpointConfigurationTypes": "REGIONAL" + } + } + }, + "MyApiWithLambdaRequestAuthDeployment9c20de6c65": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "RestApiId": { + "Ref": "MyApiWithLambdaRequestAuth" + }, + "Description": "RestApi deployment id: 9c20de6c65c8aa8750d3136af13b9a69bc7d3e5e", + "StageName": "Stage" + } + }, + "MyApiWithLambdaTokenAuthDeployment4f66714fd8": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "RestApiId": { + "Ref": "MyApiWithLambdaTokenAuth" + }, + "Description": "RestApi deployment id: 4f66714fd88af2798cc2462bd8ce435aa77a340c", + "StageName": "Stage" + } + }, + "MyApiWithLambdaRequestAuthMyLambdaRequestAuthAuthorizerPermission": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "Principal": "apigateway.amazonaws.com", + "FunctionName": { + "Fn::GetAtt": [ + "MyAuthFn", + "Arn" + ] + }, + "SourceArn": { + "Fn::Sub": [ + "arn:aws-us-gov:execute-api:${AWS::Region}:${AWS::AccountId}:${__ApiId__}/authorizers/*", + { + "__ApiId__": { + "Ref": "MyApiWithLambdaRequestAuth" + } + } + ] + } + } + }, + "MyApiWithCognitoAuthDeploymentbac15a89c4": { + "Type": "AWS::ApiGateway::Deployment", + "Properties": { + "RestApiId": { + "Ref": "MyApiWithCognitoAuth" + }, + "Description": "RestApi deployment id: bac15a89c4ef70c7a908f93d9f39dc7ce56fa1e3", + "StageName": "Stage" + } + }, + "MyApiWithLambdaRequestAuthProdStage": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "DeploymentId": { + "Ref": "MyApiWithLambdaRequestAuthDeployment9c20de6c65" + }, + "RestApiId": { + "Ref": "MyApiWithLambdaRequestAuth" + }, + "StageName": "Prod" + } + }, + "MyFnLambdaTokenPermissionProd": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "Principal": "apigateway.amazonaws.com", + "FunctionName": { + "Ref": "MyFn" + }, + "SourceArn": { + "Fn::Sub": [ + "arn:aws-us-gov:execute-api:${AWS::Region}:${AWS::AccountId}:${__ApiId__}/${__Stage__}/GET/lambda-token", + { + "__Stage__": "*", + "__ApiId__": { + "Ref": "MyApiWithLambdaTokenAuth" + } + } + ] + } + } + }, + "MyFnLambdaRequestPermissionProd": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "Principal": "apigateway.amazonaws.com", + "FunctionName": { + "Ref": "MyFn" + }, + "SourceArn": { + "Fn::Sub": [ + "arn:aws-us-gov:execute-api:${AWS::Region}:${AWS::AccountId}:${__ApiId__}/${__Stage__}/GET/lambda-request", + { + "__Stage__": "*", + "__ApiId__": { + "Ref": "MyApiWithLambdaRequestAuth" + } + } + ] + } + } + }, + "MyApiWithLambdaTokenAuth": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "Body": { + "info": { + "version": "1.0", + "title": { + "Ref": "AWS::StackName" + } + }, + "paths": { + "/lambda-token": { + "get": { + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "type": "aws_proxy", + "uri": { + "Fn::Sub": "arn:aws-us-gov:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${MyFn.Arn}/invocations" + } + }, + "security": [ + { + "NONE": [] + } + ], + "responses": {} + } + } + }, + "swagger": "2.0", + "securityDefinitions": { + "MyLambdaTokenAuth": { + "in": "header", + "type": "apiKey", + "name": "Authorization", + "x-amazon-apigateway-authorizer": { + "type": "token", + "authorizerUri": { + "Fn::Sub": [ + "arn:aws-us-gov:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${__FunctionArn__}/invocations", + { + "__FunctionArn__": { + "Fn::GetAtt": [ + "MyAuthFn", + "Arn" + ] + } + } ] } - ], - "type": "cognito_user_pools" - }, - "x-amazon-apigateway-authtype": "cognito_user_pools" + }, + "x-amazon-apigateway-authtype": "custom" + } } + }, + "EndpointConfiguration": { + "Types": [ + "REGIONAL" + ] + }, + "Parameters": { + "endpointConfigurationTypes": "REGIONAL" } - }, - "EndpointConfiguration": { - "Types": [ - "REGIONAL" - ] - }, - "Parameters": { - "endpointConfigurationTypes": "REGIONAL" } - } - }, - "MyApiWithLambdaRequestAuthDeployment9c20de6c65": { - "Type": "AWS::ApiGateway::Deployment", - "Properties": { - "RestApiId": { - "Ref": "MyApiWithLambdaRequestAuth" - }, - "Description": "RestApi deployment id: 9c20de6c65c8aa8750d3136af13b9a69bc7d3e5e", - "StageName": "Stage" - } - }, - "MyApiWithLambdaTokenAuthDeployment4f66714fd8": { - "Type": "AWS::ApiGateway::Deployment", - "Properties": { - "RestApiId": { - "Ref": "MyApiWithLambdaTokenAuth" - }, - "Description": "RestApi deployment id: 4f66714fd88af2798cc2462bd8ce435aa77a340c", - "StageName": "Stage" - } - }, - "MyApiWithLambdaRequestAuthMyLambdaRequestAuthAuthorizerPermission": { - "Type": "AWS::Lambda::Permission", - "Properties": { - "Action": "lambda:InvokeFunction", - "Principal": "apigateway.amazonaws.com", - "FunctionName": { - "Fn::GetAtt": [ - "MyAuthFn", - "Arn" - ] - }, - "SourceArn": { - "Fn::Sub": [ - "arn:aws-us-gov:execute-api:${AWS::Region}:${AWS::AccountId}:${__ApiId__}/authorizers/*", - { - "__ApiId__": { - "Ref": "MyApiWithLambdaRequestAuth" - } - } - ] + }, + "MyApiWithLambdaTokenAuthProdStage": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "DeploymentId": { + "Ref": "MyApiWithLambdaTokenAuthDeployment4f66714fd8" + }, + "RestApiId": { + "Ref": "MyApiWithLambdaTokenAuth" + }, + "StageName": "Prod" } - } - }, - "MyApiWithCognitoAuthDeploymentbac15a89c4": { - "Type": "AWS::ApiGateway::Deployment", - "Properties": { - "RestApiId": { - "Ref": "MyApiWithCognitoAuth" - }, - "Description": "RestApi deployment id: bac15a89c4ef70c7a908f93d9f39dc7ce56fa1e3", - "StageName": "Stage" - } - }, - "MyApiWithLambdaRequestAuthProdStage": { - "Type": "AWS::ApiGateway::Stage", - "Properties": { - "DeploymentId": { - "Ref": "MyApiWithLambdaRequestAuthDeployment9c20de6c65" - }, - "RestApiId": { - "Ref": "MyApiWithLambdaRequestAuth" - }, - "StageName": "Prod" - } - }, - "MyFnLambdaTokenPermissionProd": { - "Type": "AWS::Lambda::Permission", - "Properties": { - "Action": "lambda:InvokeFunction", - "Principal": "apigateway.amazonaws.com", - "FunctionName": { - "Ref": "MyFn" - }, - "SourceArn": { - "Fn::Sub": [ - "arn:aws-us-gov:execute-api:${AWS::Region}:${AWS::AccountId}:${__ApiId__}/${__Stage__}/GET/lambda-token", + }, + "MyUserPool": { + "Type": "AWS::Cognito::UserPool", + "Properties": { + "UsernameAttributes": [ + "email" + ], + "UserPoolName": "UserPoolName", + "Policies": { + "PasswordPolicy": { + "MinimumLength": 8 + } + }, + "Schema": [ { - "__Stage__": "*", - "__ApiId__": { - "Ref": "MyApiWithLambdaTokenAuth" - } + "AttributeDataType": "String", + "Required": false, + "Name": "email" } ] } - } - }, - "MyFnLambdaRequestPermissionProd": { - "Type": "AWS::Lambda::Permission", - "Properties": { - "Action": "lambda:InvokeFunction", - "Principal": "apigateway.amazonaws.com", - "FunctionName": { - "Ref": "MyFn" - }, - "SourceArn": { - "Fn::Sub": [ - "arn:aws-us-gov:execute-api:${AWS::Region}:${AWS::AccountId}:${__ApiId__}/${__Stage__}/GET/lambda-request", + }, + "MyAuthFn": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Handler": "index.handler", + "Code": { + "ZipFile": "exports.handler = async (event) => {\n return {\n statusCode: 200,\n body: JSON.stringify(event),\n headers: {}\n }\n}\n" + }, + "Role": { + "Fn::GetAtt": [ + "MyAuthFnRole", + "Arn" + ] + }, + "Runtime": "nodejs12.x", + "Tags": [ { - "__Stage__": "*", - "__ApiId__": { - "Ref": "MyApiWithLambdaRequestAuth" - } + "Value": "SAM", + "Key": "lambda:createdBy" } ] } - } - }, - "MyApiWithLambdaTokenAuth": { - "Type": "AWS::ApiGateway::RestApi", - "Properties": { - "Body": { - "info": { - "version": "1.0", - "title": { - "Ref": "AWS::StackName" - } - }, - "paths": { - "/lambda-token": { - "get": { - "x-amazon-apigateway-integration": { - "httpMethod": "POST", - "type": "aws_proxy", - "uri": { - "Fn::Sub": "arn:aws-us-gov:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${MyFn.Arn}/invocations" - } - }, - "security": [ - { - "NONE": [] - } - ], - "responses": {} + }, + "MyApiWithLambdaRequestAuth": { + "Type": "AWS::ApiGateway::RestApi", + "Properties": { + "Body": { + "info": { + "version": "1.0", + "title": { + "Ref": "AWS::StackName" } - } - }, - "swagger": "2.0", - "securityDefinitions": { - "MyLambdaTokenAuth": { - "in": "header", - "type": "apiKey", - "name": "Authorization", - "x-amazon-apigateway-authorizer": { - "type": "token", - "authorizerUri": { - "Fn::Sub": [ - "arn:aws-us-gov:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${__FunctionArn__}/invocations", + }, + "paths": { + "/lambda-request": { + "get": { + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "passthroughBehavior": "when_no_match", + "type": "aws_proxy", + "uri": { + "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${MyFn.Arn}/invocations" + } + }, + "security": [ { - "__FunctionArn__": { - "Fn::GetAtt": [ - "MyAuthFn", - "Arn" - ] - } + "NONE": [] } - ] + ], + "responses": {} } - }, - "x-amazon-apigateway-authtype": "custom" + } + }, + "swagger": 2.0, + "schemes": [ + "https" + ], + "securityDefinitions": { + "MyLambdaRequestAuth": { + "in": "header", + "type": "apiKey", + "name": "Unused", + "x-amazon-apigateway-authorizer": { + "type": "request", + "identitySource": "method.request.header.Authorization1", + "authorizerUri": { + "Fn::Sub": [ + "arn:aws-us-gov:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${__FunctionArn__}/invocations", + { + "__FunctionArn__": { + "Fn::GetAtt": [ + "MyAuthFn", + "Arn" + ] + } + } + ] + } + }, + "x-amazon-apigateway-authtype": "custom" + } } + }, + "EndpointConfiguration": { + "Types": [ + "REGIONAL" + ] + }, + "Parameters": { + "endpointConfigurationTypes": "REGIONAL" } - }, - "EndpointConfiguration": { - "Types": [ - "REGIONAL" - ] - }, - "Parameters": { - "endpointConfigurationTypes": "REGIONAL" } - } - }, - "MyApiWithLambdaTokenAuthProdStage": { - "Type": "AWS::ApiGateway::Stage", - "Properties": { - "DeploymentId": { - "Ref": "MyApiWithLambdaTokenAuthDeployment4f66714fd8" - }, - "RestApiId": { - "Ref": "MyApiWithLambdaTokenAuth" - }, - "StageName": "Prod" - } - }, - "MyUserPool": { - "Type": "AWS::Cognito::UserPool", - "Properties": { - "UsernameAttributes": [ - "email" - ], - "UserPoolName": "UserPoolName", - "Policies": { - "PasswordPolicy": { - "MinimumLength": 8 - } - }, - "Schema": [ - { - "AttributeDataType": "String", - "Required": false, - "Name": "email" - } - ] - } - }, - "MyAuthFn": { - "Type": "AWS::Lambda::Function", - "Properties": { - "Handler": "index.handler", - "Code": { - "ZipFile": "exports.handler = async (event) => {\n return {\n statusCode: 200,\n body: JSON.stringify(event),\n headers: {}\n }\n}\n" - }, - "Role": { - "Fn::GetAtt": [ - "MyAuthFnRole", - "Arn" - ] - }, - "Runtime": "nodejs12.x", - "Tags": [ - { - "Value": "SAM", - "Key": "lambda:createdBy" - } - ] - } - }, - "MyApiWithLambdaRequestAuth": { - "Type": "AWS::ApiGateway::RestApi", - "Properties": { - "Body": { - "info": { - "version": "1.0", - "title": { - "Ref": "AWS::StackName" - } + }, + "MyApiWithLambdaTokenAuthMyLambdaTokenAuthAuthorizerPermission": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "Principal": "apigateway.amazonaws.com", + "FunctionName": { + "Fn::GetAtt": [ + "MyAuthFn", + "Arn" + ] }, - "paths": { - "/lambda-request": { - "get": { - "x-amazon-apigateway-integration": { - "httpMethod": "POST", - "passthroughBehavior": "when_no_match", - "type": "aws_proxy", - "uri": { - "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${MyFn.Arn}/invocations" - } - }, - "security": [ - { - "NONE": [] - } - ], - "responses": {} + "SourceArn": { + "Fn::Sub": [ + "arn:aws-us-gov:execute-api:${AWS::Region}:${AWS::AccountId}:${__ApiId__}/authorizers/*", + { + "__ApiId__": { + "Ref": "MyApiWithLambdaTokenAuth" + } } + ] + } + } + }, + "MyAuthFnRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "ManagedPolicyArns": [ + "arn:aws-us-gov:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "Tags": [ + { + "Value": "SAM", + "Key": "lambda:createdBy" } - }, - "swagger": 2.0, - "schemes": [ - "https" ], - "securityDefinitions": { - "MyLambdaRequestAuth": { - "in": "header", - "type": "apiKey", - "name": "Unused", - "x-amazon-apigateway-authorizer": { - "type": "request", - "identitySource": "method.request.header.Authorization1", - "authorizerUri": { - "Fn::Sub": [ - "arn:aws-us-gov:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${__FunctionArn__}/invocations", - { - "__FunctionArn__": { - "Fn::GetAtt": [ - "MyAuthFn", - "Arn" - ] - } - } + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" ] } - }, - "x-amazon-apigateway-authtype": "custom" - } + } + ] } - }, - "EndpointConfiguration": { - "Types": [ - "REGIONAL" - ] - }, - "Parameters": { - "endpointConfigurationTypes": "REGIONAL" } - } - }, - "MyApiWithLambdaTokenAuthMyLambdaTokenAuthAuthorizerPermission": { - "Type": "AWS::Lambda::Permission", - "Properties": { - "Action": "lambda:InvokeFunction", - "Principal": "apigateway.amazonaws.com", - "FunctionName": { - "Fn::GetAtt": [ - "MyAuthFn", - "Arn" - ] - }, - "SourceArn": { - "Fn::Sub": [ - "arn:aws-us-gov:execute-api:${AWS::Region}:${AWS::AccountId}:${__ApiId__}/authorizers/*", + }, + "MyFnRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "ManagedPolicyArns": [ + "arn:aws-us-gov:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "Tags": [ { - "__ApiId__": { - "Ref": "MyApiWithLambdaTokenAuth" - } + "Value": "SAM", + "Key": "lambda:createdBy" } - ] - } - } - }, - "MyAuthFnRole": { - "Type": "AWS::IAM::Role", - "Properties": { - "ManagedPolicyArns": [ - "arn:aws-us-gov:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" - ], - "Tags": [ - { - "Value": "SAM", - "Key": "lambda:createdBy" - } - ], - "AssumeRolePolicyDocument": { - "Version": "2012-10-17", - "Statement": [ - { - "Action": [ - "sts:AssumeRole" - ], - "Effect": "Allow", - "Principal": { - "Service": [ - "lambda.amazonaws.com" - ] + ], + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } } - } - ] - } - } - }, - "MyFnRole": { - "Type": "AWS::IAM::Role", - "Properties": { - "ManagedPolicyArns": [ - "arn:aws-us-gov:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" - ], - "Tags": [ - { - "Value": "SAM", - "Key": "lambda:createdBy" + ] } - ], - "AssumeRolePolicyDocument": { - "Version": "2012-10-17", - "Statement": [ + } + }, + "MyApiWithCognitoAuthProdStage": { + "Type": "AWS::ApiGateway::Stage", + "Properties": { + "DeploymentId": { + "Ref": "MyApiWithCognitoAuthDeploymentbac15a89c4" + }, + "RestApiId": { + "Ref": "MyApiWithCognitoAuth" + }, + "StageName": "Prod" + } + }, + "MyFn": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Handler": "index.handler", + "Code": { + "ZipFile": "exports.handler = async (event) => {\n return {\n statusCode: 200,\n body: JSON.stringify(event),\n headers: {}\n }\n}\n" + }, + "Role": { + "Fn::GetAtt": [ + "MyFnRole", + "Arn" + ] + }, + "Runtime": "nodejs12.x", + "Tags": [ { - "Action": [ - "sts:AssumeRole" - ], - "Effect": "Allow", - "Principal": { - "Service": [ - "lambda.amazonaws.com" - ] - } + "Value": "SAM", + "Key": "lambda:createdBy" } ] } } - }, - "MyApiWithCognitoAuthProdStage": { - "Type": "AWS::ApiGateway::Stage", - "Properties": { - "DeploymentId": { - "Ref": "MyApiWithCognitoAuthDeploymentbac15a89c4" - }, - "RestApiId": { - "Ref": "MyApiWithCognitoAuth" - }, - "StageName": "Prod" - } - }, - "MyFn": { - "Type": "AWS::Lambda::Function", - "Properties": { - "Handler": "index.handler", - "Code": { - "ZipFile": "exports.handler = async (event) => {\n return {\n statusCode: 200,\n body: JSON.stringify(event),\n headers: {}\n }\n}\n" - }, - "Role": { - "Fn::GetAtt": [ - "MyFnRole", - "Arn" - ] - }, - "Runtime": "nodejs12.x", - "Tags": [ - { - "Value": "SAM", - "Key": "lambda:createdBy" - } - ] - } } - } -} \ No newline at end of file + } \ No newline at end of file diff --git a/tests/translator/output/aws-us-gov/api_with_usageplans_intrinsics.json b/tests/translator/output/aws-us-gov/api_with_usageplans_intrinsics.json index 0a60e70b8..0a9355d91 100644 --- a/tests/translator/output/aws-us-gov/api_with_usageplans_intrinsics.json +++ b/tests/translator/output/aws-us-gov/api_with_usageplans_intrinsics.json @@ -68,7 +68,8 @@ }, "DependsOn": [ "MyApiOne" - ] + ], + "Condition": "C1" }, "MyApiOneDeployment8b73115419": { "Type": "AWS::ApiGateway::Deployment", @@ -140,7 +141,8 @@ }, "DependsOn": [ "MyApiOneUsagePlan" - ] + ], + "Condition": "C1" }, "MyFunctionTwoRole": { "Type": "AWS::IAM::Role", @@ -185,7 +187,8 @@ }, "DependsOn": [ "MyApiOneApiKey" - ] + ], + "Condition": "C1" }, "MyApiTwoApiKey": { "Type": "AWS::ApiGateway::ApiKey", diff --git a/tests/translator/output/aws-us-gov/function_event_conditions.json b/tests/translator/output/aws-us-gov/function_event_conditions.json index e22637ef5..d35bfe484 100644 --- a/tests/translator/output/aws-us-gov/function_event_conditions.json +++ b/tests/translator/output/aws-us-gov/function_event_conditions.json @@ -529,6 +529,7 @@ "Condition": "MyCondition" }, "MyAwesomeFunctionAnotherSNSWithSQSSubscriptionQueue": { + "Condition": "MyCondition", "Type": "AWS::SQS::Queue", "Properties": {} }, diff --git a/tests/translator/output/aws-us-gov/implicit_api_deletion_policy_precedence.json b/tests/translator/output/aws-us-gov/implicit_api_deletion_policy_precedence.json new file mode 100644 index 000000000..434cc36b0 --- /dev/null +++ b/tests/translator/output/aws-us-gov/implicit_api_deletion_policy_precedence.json @@ -0,0 +1,242 @@ +{ + "Resources": { + "RestApiFunction": { + "Type": "AWS::Lambda::Function", + "DeletionPolicy": "Delete", + "UpdateReplacePolicy": "Retain", + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "todo_list.zip" + }, + "Handler": "index.restapi", + "Role": { + "Fn::GetAtt": [ + "RestApiFunctionRole", + "Arn" + ] + }, + "Runtime": "nodejs12.x", + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "RestApiFunctionRole": { + "Type": "AWS::IAM::Role", + "DeletionPolicy": "Delete", + "UpdateReplacePolicy": "Retain", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + }, + "ManagedPolicyArns": [ + "arn:aws-us-gov:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", + "arn:aws-us-gov:iam::aws:policy/AmazonDynamoDBFullAccess" + ], + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "RestApiFunctionGetHtmlPermissionProd": { + "Type": "AWS::Lambda::Permission", + "DeletionPolicy": "Delete", + "UpdateReplacePolicy": "Retain", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Ref": "RestApiFunction" + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Sub": [ + "arn:aws-us-gov:execute-api:${AWS::Region}:${AWS::AccountId}:${__ApiId__}/${__Stage__}/*/*", + { + "__ApiId__": { + "Ref": "ServerlessRestApi" + }, + "__Stage__": "*" + } + ] + } + } + }, + "GetHtmlFunction": { + "Type": "AWS::Lambda::Function", + "DeletionPolicy": "Retain", + "UpdateReplacePolicy": "Retain", + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "todo_list.zip" + }, + "Handler": "index.gethtml", + "Role": { + "Fn::GetAtt": [ + "GetHtmlFunctionRole", + "Arn" + ] + }, + "Runtime": "nodejs12.x", + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "GetHtmlFunctionRole": { + "Type": "AWS::IAM::Role", + "DeletionPolicy": "Retain", + "UpdateReplacePolicy": "Retain", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + }, + "ManagedPolicyArns": [ + "arn:aws-us-gov:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", + "arn:aws-us-gov:iam::aws:policy/AmazonDynamoDBReadOnlyAccess" + ], + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "GetHtmlFunctionGetHtmlPermissionProd": { + "Type": "AWS::Lambda::Permission", + "DeletionPolicy": "Retain", + "UpdateReplacePolicy": "Retain", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Ref": "GetHtmlFunction" + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Sub": [ + "arn:aws-us-gov:execute-api:${AWS::Region}:${AWS::AccountId}:${__ApiId__}/${__Stage__}/*/{proxy++}", + { + "__ApiId__": { + "Ref": "ServerlessRestApi" + }, + "__Stage__": "*" + } + ] + } + } + }, + "ServerlessRestApi": { + "Type": "AWS::ApiGateway::RestApi", + "DeletionPolicy": "Retain", + "UpdateReplacePolicy": "Retain", + "Properties": { + "Body": { + "swagger": "2.0", + "info": { + "version": "1.0", + "title": { + "Ref": "AWS::StackName" + } + }, + "paths": { + "/{proxy+}": { + "x-amazon-apigateway-any-method": { + "x-amazon-apigateway-integration": { + "type": "aws_proxy", + "httpMethod": "POST", + "uri": { + "Fn::Sub": "arn:aws-us-gov:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${RestApiFunction.Arn}/invocations" + } + }, + "responses": {} + } + }, + "/{proxy++}": { + "x-amazon-apigateway-any-method": { + "x-amazon-apigateway-integration": { + "type": "aws_proxy", + "httpMethod": "POST", + "uri": { + "Fn::Sub": "arn:aws-us-gov:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${GetHtmlFunction.Arn}/invocations" + } + }, + "responses": {} + } + } + } + }, + "EndpointConfiguration": { + "Types": [ + "REGIONAL" + ] + }, + "Parameters": { + "endpointConfigurationTypes": "REGIONAL" + } + } + }, + "ServerlessRestApiDeployment695a271271": { + "Type": "AWS::ApiGateway::Deployment", + "DeletionPolicy": "Retain", + "UpdateReplacePolicy": "Retain", + "Properties": { + "Description": "RestApi deployment id: 695a271271d114dfef4fba262c839d269473d910", + "RestApiId": { + "Ref": "ServerlessRestApi" + }, + "StageName": "Stage" + } + }, + "ServerlessRestApiProdStage": { + "Type": "AWS::ApiGateway::Stage", + "DeletionPolicy": "Retain", + "UpdateReplacePolicy": "Retain", + "Properties": { + "DeploymentId": { + "Ref": "ServerlessRestApiDeployment695a271271" + }, + "RestApiId": { + "Ref": "ServerlessRestApi" + }, + "StageName": "Prod" + } + } + } +} \ No newline at end of file diff --git a/tests/translator/output/aws-us-gov/layer_deletion_policy_precedence.json b/tests/translator/output/aws-us-gov/layer_deletion_policy_precedence.json new file mode 100644 index 000000000..8bb610ad0 --- /dev/null +++ b/tests/translator/output/aws-us-gov/layer_deletion_policy_precedence.json @@ -0,0 +1,37 @@ +{ + "Resources": { + "MinimalLayer22b6609c3d": { + "Type": "AWS::Lambda::LayerVersion", + "DeletionPolicy": "Retain", + "Properties": { + "Content": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "layer.zip" + }, + "LayerName": "MinimalLayer" + } + }, + "MinimalLayer2800a44a445": { + "Type": "AWS::Lambda::LayerVersion", + "DeletionPolicy": "Delete", + "Properties": { + "Content": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "layer.zip" + }, + "LayerName": "MinimalLayer2" + } + }, + "MinimalLayer3ac07350a04": { + "Type": "AWS::Lambda::LayerVersion", + "DeletionPolicy": "Retain", + "Properties": { + "Content": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "layer.zip" + }, + "LayerName": "MinimalLayer3" + } + } + } +} \ No newline at end of file diff --git a/tests/translator/output/aws-us-gov/version_deletion_policy_precedence.json b/tests/translator/output/aws-us-gov/version_deletion_policy_precedence.json new file mode 100644 index 000000000..0beb66af0 --- /dev/null +++ b/tests/translator/output/aws-us-gov/version_deletion_policy_precedence.json @@ -0,0 +1,163 @@ +{ + "Resources": { + "MinimalFunction": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "hello.zip" + }, + "Handler": "hello.handler", + "Role": { + "Fn::GetAtt": [ + "MinimalFunctionRole", + "Arn" + ] + }, + "Runtime": "python2.7", + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "MinimalFunctionVersion640128d35d": { + "Type": "AWS::Lambda::Version", + "DeletionPolicy": "Retain", + "Properties": { + "Description": "sam-testing", + "FunctionName": { + "Ref": "MinimalFunction" + } + } + }, + "MinimalFunctionAliaslive": { + "Type": "AWS::Lambda::Alias", + "Properties": { + "Name": "live", + "FunctionName": { + "Ref": "MinimalFunction" + }, + "FunctionVersion": { + "Fn::GetAtt": [ + "MinimalFunctionVersion640128d35d", + "Version" + ] + } + } + }, + "MinimalFunctionRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + }, + "ManagedPolicyArns": [ + "arn:aws-us-gov:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "MinimalFunction2": { + "Type": "AWS::Lambda::Function", + "DeletionPolicy": "Delete", + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "hello.zip" + }, + "Handler": "hello.handler", + "Role": { + "Fn::GetAtt": [ + "MinimalFunction2Role", + "Arn" + ] + }, + "Runtime": "python2.7", + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "MinimalFunction2Version640128d35d": { + "Type": "AWS::Lambda::Version", + "DeletionPolicy": "Delete", + "Properties": { + "Description": "sam-testing", + "FunctionName": { + "Ref": "MinimalFunction2" + } + } + }, + "MinimalFunction2Aliaslive": { + "Type": "AWS::Lambda::Alias", + "DeletionPolicy": "Delete", + "Properties": { + "Name": "live", + "FunctionName": { + "Ref": "MinimalFunction2" + }, + "FunctionVersion": { + "Fn::GetAtt": [ + "MinimalFunction2Version640128d35d", + "Version" + ] + } + } + }, + "MinimalFunction2Role": { + "Type": "AWS::IAM::Role", + "DeletionPolicy": "Delete", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + }, + "ManagedPolicyArns": [ + "arn:aws-us-gov:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + } + } +} \ No newline at end of file diff --git a/tests/translator/output/function_event_conditions.json b/tests/translator/output/function_event_conditions.json index c6de70318..4926995a9 100644 --- a/tests/translator/output/function_event_conditions.json +++ b/tests/translator/output/function_event_conditions.json @@ -529,6 +529,7 @@ "Condition": "MyCondition" }, "MyAwesomeFunctionAnotherSNSWithSQSSubscriptionQueue": { + "Condition": "MyCondition", "Type": "AWS::SQS::Queue", "Properties": {} }, diff --git a/tests/translator/output/implicit_api_deletion_policy_precedence.json b/tests/translator/output/implicit_api_deletion_policy_precedence.json new file mode 100644 index 000000000..025e04735 --- /dev/null +++ b/tests/translator/output/implicit_api_deletion_policy_precedence.json @@ -0,0 +1,234 @@ +{ + "Resources": { + "RestApiFunction": { + "Type": "AWS::Lambda::Function", + "DeletionPolicy": "Delete", + "UpdateReplacePolicy": "Retain", + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "todo_list.zip" + }, + "Handler": "index.restapi", + "Role": { + "Fn::GetAtt": [ + "RestApiFunctionRole", + "Arn" + ] + }, + "Runtime": "nodejs12.x", + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "RestApiFunctionRole": { + "Type": "AWS::IAM::Role", + "DeletionPolicy": "Delete", + "UpdateReplacePolicy": "Retain", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + }, + "ManagedPolicyArns": [ + "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", + "arn:aws:iam::aws:policy/AmazonDynamoDBFullAccess" + ], + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "RestApiFunctionGetHtmlPermissionProd": { + "Type": "AWS::Lambda::Permission", + "DeletionPolicy": "Delete", + "UpdateReplacePolicy": "Retain", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Ref": "RestApiFunction" + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Sub": [ + "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${__ApiId__}/${__Stage__}/*/*", + { + "__ApiId__": { + "Ref": "ServerlessRestApi" + }, + "__Stage__": "*" + } + ] + } + } + }, + "GetHtmlFunction": { + "Type": "AWS::Lambda::Function", + "DeletionPolicy": "Retain", + "UpdateReplacePolicy": "Retain", + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "todo_list.zip" + }, + "Handler": "index.gethtml", + "Role": { + "Fn::GetAtt": [ + "GetHtmlFunctionRole", + "Arn" + ] + }, + "Runtime": "nodejs12.x", + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "GetHtmlFunctionRole": { + "Type": "AWS::IAM::Role", + "DeletionPolicy": "Retain", + "UpdateReplacePolicy": "Retain", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + }, + "ManagedPolicyArns": [ + "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", + "arn:aws:iam::aws:policy/AmazonDynamoDBReadOnlyAccess" + ], + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "GetHtmlFunctionGetHtmlPermissionProd": { + "Type": "AWS::Lambda::Permission", + "DeletionPolicy": "Retain", + "UpdateReplacePolicy": "Retain", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Ref": "GetHtmlFunction" + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Sub": [ + "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${__ApiId__}/${__Stage__}/*/{proxy++}", + { + "__ApiId__": { + "Ref": "ServerlessRestApi" + }, + "__Stage__": "*" + } + ] + } + } + }, + "ServerlessRestApi": { + "Type": "AWS::ApiGateway::RestApi", + "DeletionPolicy": "Retain", + "UpdateReplacePolicy": "Retain", + "Properties": { + "Body": { + "swagger": "2.0", + "info": { + "version": "1.0", + "title": { + "Ref": "AWS::StackName" + } + }, + "paths": { + "/{proxy+}": { + "x-amazon-apigateway-any-method": { + "x-amazon-apigateway-integration": { + "type": "aws_proxy", + "httpMethod": "POST", + "uri": { + "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${RestApiFunction.Arn}/invocations" + } + }, + "responses": {} + } + }, + "/{proxy++}": { + "x-amazon-apigateway-any-method": { + "x-amazon-apigateway-integration": { + "type": "aws_proxy", + "httpMethod": "POST", + "uri": { + "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${GetHtmlFunction.Arn}/invocations" + } + }, + "responses": {} + } + } + } + } + } + }, + "ServerlessRestApiDeploymentbeaf67e605": { + "Type": "AWS::ApiGateway::Deployment", + "DeletionPolicy": "Retain", + "UpdateReplacePolicy": "Retain", + "Properties": { + "Description": "RestApi deployment id: beaf67e605cdfc82f45c51fa5f8d9552af2ca0c6", + "RestApiId": { + "Ref": "ServerlessRestApi" + }, + "StageName": "Stage" + } + }, + "ServerlessRestApiProdStage": { + "Type": "AWS::ApiGateway::Stage", + "DeletionPolicy": "Retain", + "UpdateReplacePolicy": "Retain", + "Properties": { + "DeploymentId": { + "Ref": "ServerlessRestApiDeploymentbeaf67e605" + }, + "RestApiId": { + "Ref": "ServerlessRestApi" + }, + "StageName": "Prod" + } + } + } +} \ No newline at end of file diff --git a/tests/translator/output/layer_deletion_policy_precedence.json b/tests/translator/output/layer_deletion_policy_precedence.json new file mode 100644 index 000000000..8bb610ad0 --- /dev/null +++ b/tests/translator/output/layer_deletion_policy_precedence.json @@ -0,0 +1,37 @@ +{ + "Resources": { + "MinimalLayer22b6609c3d": { + "Type": "AWS::Lambda::LayerVersion", + "DeletionPolicy": "Retain", + "Properties": { + "Content": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "layer.zip" + }, + "LayerName": "MinimalLayer" + } + }, + "MinimalLayer2800a44a445": { + "Type": "AWS::Lambda::LayerVersion", + "DeletionPolicy": "Delete", + "Properties": { + "Content": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "layer.zip" + }, + "LayerName": "MinimalLayer2" + } + }, + "MinimalLayer3ac07350a04": { + "Type": "AWS::Lambda::LayerVersion", + "DeletionPolicy": "Retain", + "Properties": { + "Content": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "layer.zip" + }, + "LayerName": "MinimalLayer3" + } + } + } +} \ No newline at end of file diff --git a/tests/translator/output/version_deletion_policy_precedence.json b/tests/translator/output/version_deletion_policy_precedence.json new file mode 100644 index 000000000..36ab3d842 --- /dev/null +++ b/tests/translator/output/version_deletion_policy_precedence.json @@ -0,0 +1,163 @@ +{ + "Resources": { + "MinimalFunction": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "hello.zip" + }, + "Handler": "hello.handler", + "Role": { + "Fn::GetAtt": [ + "MinimalFunctionRole", + "Arn" + ] + }, + "Runtime": "python2.7", + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "MinimalFunctionVersion640128d35d": { + "Type": "AWS::Lambda::Version", + "DeletionPolicy": "Retain", + "Properties": { + "Description": "sam-testing", + "FunctionName": { + "Ref": "MinimalFunction" + } + } + }, + "MinimalFunctionAliaslive": { + "Type": "AWS::Lambda::Alias", + "Properties": { + "Name": "live", + "FunctionName": { + "Ref": "MinimalFunction" + }, + "FunctionVersion": { + "Fn::GetAtt": [ + "MinimalFunctionVersion640128d35d", + "Version" + ] + } + } + }, + "MinimalFunctionRole": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + }, + "ManagedPolicyArns": [ + "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "MinimalFunction2": { + "Type": "AWS::Lambda::Function", + "DeletionPolicy": "Delete", + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "hello.zip" + }, + "Handler": "hello.handler", + "Role": { + "Fn::GetAtt": [ + "MinimalFunction2Role", + "Arn" + ] + }, + "Runtime": "python2.7", + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + }, + "MinimalFunction2Version640128d35d": { + "Type": "AWS::Lambda::Version", + "DeletionPolicy": "Delete", + "Properties": { + "Description": "sam-testing", + "FunctionName": { + "Ref": "MinimalFunction2" + } + } + }, + "MinimalFunction2Aliaslive": { + "Type": "AWS::Lambda::Alias", + "DeletionPolicy": "Delete", + "Properties": { + "Name": "live", + "FunctionName": { + "Ref": "MinimalFunction2" + }, + "FunctionVersion": { + "Fn::GetAtt": [ + "MinimalFunction2Version640128d35d", + "Version" + ] + } + } + }, + "MinimalFunction2Role": { + "Type": "AWS::IAM::Role", + "DeletionPolicy": "Delete", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ] + }, + "ManagedPolicyArns": [ + "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + } + } + } +} \ No newline at end of file diff --git a/tests/translator/test_resource_level_attributes.py b/tests/translator/test_resource_level_attributes.py new file mode 100644 index 000000000..58754fe92 --- /dev/null +++ b/tests/translator/test_resource_level_attributes.py @@ -0,0 +1,86 @@ +import itertools +from mock import patch + +from parameterized import parameterized + +from tests.plugins.application.test_serverless_app_plugin import mock_get_region +from tests.translator.test_translator import mock_sar_service_call, AbstractTestTranslator + + +class TestResourceLevelAttributes(AbstractTestTranslator): + @parameterized.expand( + itertools.product( + [ + "cognito_userpool_with_event", + "s3_with_condition", + "function_with_condition", + "basic_function", + "basic_application", + "application_with_intrinsics", + "cloudwatchevent", + "eventbridgerule", + "cloudwatchlog", + "streams", + "sqs", + "function_with_amq", + "simpletable", + "implicit_api", + "explicit_api", + "api_description", + "s3", + "sns", + "alexa_skill", + "iot_rule", + "layers_all_properties", + "unsupported_resources", + "intrinsic_functions", + "basic_function_with_tags", + "depends_on", + "function_event_conditions", + "function_with_alias", + "function_with_layers", + "global_handle_path_level_parameter", + "all_policy_templates", + "simple_table_ref_parameter_intrinsic", + "implicit_api_with_serverless_rest_api_resource", + "api_with_cors_and_conditions_no_definitionbody", + "api_with_auth_and_conditions_all_max", + "api_with_apikey_required", + "api_with_path_parameters", + "function_with_event_source_mapping", + "api_with_usageplans", + "state_machine_with_inline_definition", + "function_with_file_system_config", + "state_machine_with_permissions_boundary", + ], + [ + ("aws", "ap-southeast-1"), + ("aws-cn", "cn-north-1"), + ("aws-us-gov", "us-gov-west-1"), + ], # Run all the above tests against each of the list of partitions to test against + ) + ) + @patch( + "samtranslator.plugins.application.serverless_app_plugin.ServerlessAppPlugin._sar_service_call", + mock_sar_service_call, + ) + @patch("botocore.client.ClientEndpointBridge._check_default_region", mock_get_region) + def test_transform_with_additional_resource_level_attributes(self, testcase, partition_with_region): + partition = partition_with_region[0] + region = partition_with_region[1] + + # add resource level attributes to input resources + manifest = self._read_input(testcase) + resources = manifest.get("Resources", []) + for _, resource in resources.items(): + resource["DeletionPolicy"] = "Delete" + resource["UpdateReplacePolicy"] = "Retain" + + # add resource level attributes to expected output resources + expected = self._read_expected_output(testcase, partition) + expected_resources = expected.get("Resources", []) + for _, expected_resource in expected_resources.items(): + expected_resource["DeletionPolicy"] = "Delete" + expected_resource["UpdateReplacePolicy"] = "Retain" + + self._compare_transform(manifest, expected, partition, region) diff --git a/tests/translator/test_translator.py b/tests/translator/test_translator.py index 27667adeb..9bbe916e5 100644 --- a/tests/translator/test_translator.py +++ b/tests/translator/test_translator.py @@ -138,7 +138,122 @@ def mock_sar_service_call(self, service_call_function, logical_id, *args): # api and s3 location for explicit api. -class TestTranslatorEndToEnd(TestCase): +class AbstractTestTranslator(TestCase): + def _read_input(self, testcase): + manifest = yaml_parse(open(os.path.join(INPUT_FOLDER, testcase + ".yaml"), "r")) + # To uncover unicode-related bugs, convert dict to JSON string and parse JSON back to dict + return json.loads(json.dumps(manifest)) + + def _read_expected_output(self, testcase, partition): + partition_folder = partition if partition != "aws" else "" + expected_filepath = os.path.join(OUTPUT_FOLDER, partition_folder, testcase + ".json") + return json.load(open(expected_filepath, "r")) + + def _compare_transform(self, manifest, expected, partition, region): + with patch("boto3.session.Session.region_name", region): + parameter_values = get_template_parameter_values() + mock_policy_loader = MagicMock() + mock_policy_loader.load.return_value = { + "AWSLambdaBasicExecutionRole": "arn:{}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole".format( + partition + ), + "AmazonDynamoDBFullAccess": "arn:{}:iam::aws:policy/AmazonDynamoDBFullAccess".format(partition), + "AmazonDynamoDBReadOnlyAccess": "arn:{}:iam::aws:policy/AmazonDynamoDBReadOnlyAccess".format(partition), + "AWSLambdaRole": "arn:{}:iam::aws:policy/service-role/AWSLambdaRole".format(partition), + } + if partition == "aws": + mock_policy_loader.load.return_value[ + "AWSXrayWriteOnlyAccess" + ] = "arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess" + else: + mock_policy_loader.load.return_value[ + "AWSXRayDaemonWriteAccess" + ] = "arn:{}:iam::aws:policy/AWSXRayDaemonWriteAccess".format(partition) + + output_fragment = transform(manifest, parameter_values, mock_policy_loader) + + print(json.dumps(output_fragment, indent=2)) + + # Only update the deployment Logical Id hash in Py3. + if sys.version_info.major >= 3: + self._update_logical_id_hash(expected) + self._update_logical_id_hash(output_fragment) + + self.assertEqual(deep_sort_lists(output_fragment), deep_sort_lists(expected)) + + def _update_logical_id_hash(self, resources): + """ + Brute force method for updating all APIGW Deployment LogicalIds and references to a consistent hash + """ + output_resources = resources.get("Resources", {}) + deployment_logical_id_dict = {} + rest_api_to_swagger_hash = {} + dict_of_things_to_delete = {} + + # Find all RestApis in the template + for logical_id, resource_dict in output_resources.items(): + if "AWS::ApiGateway::RestApi" == resource_dict.get("Type"): + resource_properties = resource_dict.get("Properties", {}) + if "Body" in resource_properties: + self._generate_new_deployment_hash( + logical_id, resource_properties.get("Body"), rest_api_to_swagger_hash + ) + + elif "BodyS3Location" in resource_dict.get("Properties"): + self._generate_new_deployment_hash( + logical_id, resource_properties.get("BodyS3Location"), rest_api_to_swagger_hash + ) + + # Collect all APIGW Deployments LogicalIds and generate the new ones + for logical_id, resource_dict in output_resources.items(): + if "AWS::ApiGateway::Deployment" == resource_dict.get("Type"): + resource_properties = resource_dict.get("Properties", {}) + + rest_id = resource_properties.get("RestApiId").get("Ref") + + data_hash = rest_api_to_swagger_hash.get(rest_id) + + description = resource_properties.get("Description")[: -len(data_hash)] + + resource_properties["Description"] = description + data_hash + + new_logical_id = logical_id[:-10] + data_hash[:10] + + deployment_logical_id_dict[logical_id] = new_logical_id + dict_of_things_to_delete[logical_id] = (new_logical_id, resource_dict) + + # Update References to APIGW Deployments + for logical_id, resource_dict in output_resources.items(): + if "AWS::ApiGateway::Stage" == resource_dict.get("Type"): + resource_properties = resource_dict.get("Properties", {}) + + rest_id = resource_properties.get("RestApiId", {}).get("Ref", "") + + data_hash = rest_api_to_swagger_hash.get(rest_id) + + deployment_id = resource_properties.get("DeploymentId", {}).get("Ref") + new_logical_id = deployment_logical_id_dict.get(deployment_id, "")[:-10] + new_logical_id = new_logical_id + data_hash[:10] + + resource_properties.get("DeploymentId", {})["Ref"] = new_logical_id + + # To avoid mutating the template while iterating, delete only after find everything to update + for logical_id_to_remove, tuple_to_add in dict_of_things_to_delete.items(): + output_resources[tuple_to_add[0]] = tuple_to_add[1] + del output_resources[logical_id_to_remove] + + # Update any Output References in the template + for output_key, output_value in resources.get("Outputs", {}).items(): + if output_value.get("Value").get("Ref") in deployment_logical_id_dict: + output_value["Value"]["Ref"] = deployment_logical_id_dict[output_value.get("Value").get("Ref")] + + def _generate_new_deployment_hash(self, logical_id, dict_to_hash, rest_api_to_swagger_hash): + data_bytes = json.dumps(dict_to_hash, separators=(",", ":"), sort_keys=True).encode("utf8") + data_hash = hashlib.sha1(data_bytes).hexdigest() + rest_api_to_swagger_hash[logical_id] = data_hash + + +class TestTranslatorEndToEnd(AbstractTestTranslator): @parameterized.expand( itertools.product( [ @@ -223,6 +338,7 @@ class TestTranslatorEndToEnd(TestCase): "iot_rule", "layers_with_intrinsics", "layers_all_properties", + "layer_deletion_policy_precedence", "function_managed_inline_policy", "unsupported_resources", "intrinsic_functions", @@ -268,6 +384,7 @@ class TestTranslatorEndToEnd(TestCase): "simple_table_with_extra_tags", "explicit_api_with_invalid_events_config", "no_implicit_api_with_serverless_rest_api_resource", + "implicit_api_deletion_policy_precedence", "implicit_api_with_serverless_rest_api_resource", "implicit_api_with_auth_and_conditions_max", "implicit_api_with_many_conditions", @@ -315,6 +432,7 @@ class TestTranslatorEndToEnd(TestCase): "state_machine_with_xray_role", "function_with_file_system_config", "state_machine_with_permissions_boundary", + "version_deletion_policy_precedence", ], [ ("aws", "ap-southeast-1"), @@ -332,43 +450,10 @@ def test_transform_success(self, testcase, partition_with_region): partition = partition_with_region[0] region = partition_with_region[1] - manifest = yaml_parse(open(os.path.join(INPUT_FOLDER, testcase + ".yaml"), "r")) - # To uncover unicode-related bugs, convert dict to JSON string and parse JSON back to dict - manifest = json.loads(json.dumps(manifest)) - partition_folder = partition if partition != "aws" else "" - expected_filepath = os.path.join(OUTPUT_FOLDER, partition_folder, testcase + ".json") - expected = json.load(open(expected_filepath, "r")) + manifest = self._read_input(testcase) + expected = self._read_expected_output(testcase, partition) - with patch("boto3.session.Session.region_name", region): - parameter_values = get_template_parameter_values() - mock_policy_loader = MagicMock() - mock_policy_loader.load.return_value = { - "AWSLambdaBasicExecutionRole": "arn:{}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole".format( - partition - ), - "AmazonDynamoDBFullAccess": "arn:{}:iam::aws:policy/AmazonDynamoDBFullAccess".format(partition), - "AmazonDynamoDBReadOnlyAccess": "arn:{}:iam::aws:policy/AmazonDynamoDBReadOnlyAccess".format(partition), - "AWSLambdaRole": "arn:{}:iam::aws:policy/service-role/AWSLambdaRole".format(partition), - } - if partition == "aws": - mock_policy_loader.load.return_value[ - "AWSXrayWriteOnlyAccess" - ] = "arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess" - else: - mock_policy_loader.load.return_value[ - "AWSXRayDaemonWriteAccess" - ] = "arn:{}:iam::aws:policy/AWSXRayDaemonWriteAccess".format(partition) - - output_fragment = transform(manifest, parameter_values, mock_policy_loader) - - print(json.dumps(output_fragment, indent=2)) - - # Only update the deployment Logical Id hash in Py3. - if sys.version_info.major >= 3: - self._update_logical_id_hash(expected) - self._update_logical_id_hash(output_fragment) - - assert deep_sort_lists(output_fragment) == deep_sort_lists(expected) + self._compare_transform(manifest, expected, partition, region) @parameterized.expand( itertools.product( @@ -450,7 +535,7 @@ def test_transform_success_openapi3(self, testcase, partition_with_region): self._update_logical_id_hash(expected) self._update_logical_id_hash(output_fragment) - assert deep_sort_lists(output_fragment) == deep_sort_lists(expected) + self.assertEqual(deep_sort_lists(output_fragment), deep_sort_lists(expected)) @parameterized.expand( itertools.product( @@ -508,78 +593,8 @@ def test_transform_success_resource_policy(self, testcase, partition_with_region if sys.version_info.major >= 3: self._update_logical_id_hash(expected) self._update_logical_id_hash(output_fragment) - assert deep_sort_lists(output_fragment) == deep_sort_lists(expected) - - def _update_logical_id_hash(self, resources): - """ - Brute force method for updating all APIGW Deployment LogicalIds and references to a consistent hash - """ - output_resources = resources.get("Resources", {}) - deployment_logical_id_dict = {} - rest_api_to_swagger_hash = {} - dict_of_things_to_delete = {} - - # Find all RestApis in the template - for logical_id, resource_dict in output_resources.items(): - if "AWS::ApiGateway::RestApi" == resource_dict.get("Type"): - resource_properties = resource_dict.get("Properties", {}) - if "Body" in resource_properties: - self._generate_new_deployment_hash( - logical_id, resource_properties.get("Body"), rest_api_to_swagger_hash - ) - - elif "BodyS3Location" in resource_dict.get("Properties"): - self._generate_new_deployment_hash( - logical_id, resource_properties.get("BodyS3Location"), rest_api_to_swagger_hash - ) - - # Collect all APIGW Deployments LogicalIds and generate the new ones - for logical_id, resource_dict in output_resources.items(): - if "AWS::ApiGateway::Deployment" == resource_dict.get("Type"): - resource_properties = resource_dict.get("Properties", {}) - rest_id = resource_properties.get("RestApiId").get("Ref") - - data_hash = rest_api_to_swagger_hash.get(rest_id) - - description = resource_properties.get("Description")[: -len(data_hash)] - - resource_properties["Description"] = description + data_hash - - new_logical_id = logical_id[:-10] + data_hash[:10] - - deployment_logical_id_dict[logical_id] = new_logical_id - dict_of_things_to_delete[logical_id] = (new_logical_id, resource_dict) - - # Update References to APIGW Deployments - for logical_id, resource_dict in output_resources.items(): - if "AWS::ApiGateway::Stage" == resource_dict.get("Type"): - resource_properties = resource_dict.get("Properties", {}) - - rest_id = resource_properties.get("RestApiId", {}).get("Ref", "") - - data_hash = rest_api_to_swagger_hash.get(rest_id) - - deployment_id = resource_properties.get("DeploymentId", {}).get("Ref") - new_logical_id = deployment_logical_id_dict.get(deployment_id, "")[:-10] - new_logical_id = new_logical_id + data_hash[:10] - - resource_properties.get("DeploymentId", {})["Ref"] = new_logical_id - - # To avoid mutating the template while iterating, delete only after find everything to update - for logical_id_to_remove, tuple_to_add in dict_of_things_to_delete.items(): - output_resources[tuple_to_add[0]] = tuple_to_add[1] - del output_resources[logical_id_to_remove] - - # Update any Output References in the template - for output_key, output_value in resources.get("Outputs", {}).items(): - if output_value.get("Value").get("Ref") in deployment_logical_id_dict: - output_value["Value"]["Ref"] = deployment_logical_id_dict[output_value.get("Value").get("Ref")] - - def _generate_new_deployment_hash(self, logical_id, dict_to_hash, rest_api_to_swagger_hash): - data_bytes = json.dumps(dict_to_hash, separators=(",", ":"), sort_keys=True).encode("utf8") - data_hash = hashlib.sha1(data_bytes).hexdigest() - rest_api_to_swagger_hash[logical_id] = data_hash + self.assertEqual(deep_sort_lists(output_fragment), deep_sort_lists(expected)) @pytest.mark.parametrize( From 0823f30284826a26cda54ff2161c93a84816cbff Mon Sep 17 00:00:00 2001 From: Jacob Fuss <32497805+jfuss@users.noreply.github.com> Date: Mon, 10 May 2021 11:41:14 -0500 Subject: [PATCH 14/16] fix: Fail when Intrinsics are in SourceVPC list instead of IntrinsicsSourceVPC (#1999) --- samtranslator/swagger/swagger.py | 71 ++++++++++++------- tests/swagger/test_swagger.py | 12 ++++ ...rror_api_invalid_source_vpc_blacklist.yaml | 31 ++++++++ ...rror_api_invalid_source_vpc_whitelist.yaml | 31 ++++++++ ...rror_api_invalid_source_vpc_blacklist.json | 3 + ...rror_api_invalid_source_vpc_whitelist.json | 3 + 6 files changed, 127 insertions(+), 24 deletions(-) create mode 100644 tests/translator/input/error_api_invalid_source_vpc_blacklist.yaml create mode 100644 tests/translator/input/error_api_invalid_source_vpc_whitelist.yaml create mode 100644 tests/translator/output/error_api_invalid_source_vpc_blacklist.json create mode 100644 tests/translator/output/error_api_invalid_source_vpc_whitelist.json diff --git a/samtranslator/swagger/swagger.py b/samtranslator/swagger/swagger.py index d6aa36c2f..36c9f5d18 100644 --- a/samtranslator/swagger/swagger.py +++ b/samtranslator/swagger/swagger.py @@ -892,6 +892,8 @@ def add_resource_policy(self, resource_policy, path, api_id, stage): ip_range_blacklist = resource_policy.get("IpRangeBlacklist") source_vpc_whitelist = resource_policy.get("SourceVpcWhitelist") source_vpc_blacklist = resource_policy.get("SourceVpcBlacklist") + + # Intrinsic's supported in these properties source_vpc_intrinsic_whitelist = resource_policy.get("IntrinsicVpcWhitelist") source_vpce_intrinsic_whitelist = resource_policy.get("IntrinsicVpceWhitelist") source_vpc_intrinsic_blacklist = resource_policy.get("IntrinsicVpcBlacklist") @@ -913,31 +915,38 @@ def add_resource_policy(self, resource_policy, path, api_id, stage): resource_list = self._get_method_path_uri_list(path, api_id, stage) self._add_ip_resource_policy_for_method(ip_range_blacklist, "IpAddress", resource_list) - if ( - (source_vpc_blacklist is not None) - or (source_vpc_intrinsic_blacklist is not None) - or (source_vpce_intrinsic_blacklist is not None) - ): - blacklist_dict = { - "StringEndpointList": source_vpc_blacklist, - "IntrinsicVpcList": source_vpc_intrinsic_blacklist, - "IntrinsicVpceList": source_vpce_intrinsic_blacklist, - } - resource_list = self._get_method_path_uri_list(path, api_id, stage) - self._add_vpc_resource_policy_for_method(blacklist_dict, "StringEquals", resource_list) + if not SwaggerEditor._validate_list_property_is_resolved(source_vpc_blacklist): + raise InvalidDocumentException( + [ + InvalidTemplateException( + "SourceVpcBlacklist must be a list of strings. Use IntrinsicVpcBlacklist instead for values that use Intrinsic Functions" + ) + ] + ) - if ( - (source_vpc_whitelist is not None) - or (source_vpc_intrinsic_whitelist is not None) - or (source_vpce_intrinsic_whitelist is not None) - ): - whitelist_dict = { - "StringEndpointList": source_vpc_whitelist, - "IntrinsicVpcList": source_vpc_intrinsic_whitelist, - "IntrinsicVpceList": source_vpce_intrinsic_whitelist, - } - resource_list = self._get_method_path_uri_list(path, api_id, stage) - self._add_vpc_resource_policy_for_method(whitelist_dict, "StringNotEquals", resource_list) + blacklist_dict = { + "StringEndpointList": source_vpc_blacklist, + "IntrinsicVpcList": source_vpc_intrinsic_blacklist, + "IntrinsicVpceList": source_vpce_intrinsic_blacklist, + } + resource_list = self._get_method_path_uri_list(path, api_id, stage) + self._add_vpc_resource_policy_for_method(blacklist_dict, "StringEquals", resource_list) + + if not SwaggerEditor._validate_list_property_is_resolved(source_vpc_whitelist): + raise InvalidDocumentException( + [ + InvalidTemplateException( + "SourceVpcWhitelist must be a list of strings. Use IntrinsicVpcWhitelist instead for values that use Intrinsic Functions" + ) + ] + ) + + whitelist_dict = { + "StringEndpointList": source_vpc_whitelist, + "IntrinsicVpcList": source_vpc_intrinsic_whitelist, + "IntrinsicVpceList": source_vpce_intrinsic_whitelist, + } + self._add_vpc_resource_policy_for_method(whitelist_dict, "StringNotEquals", resource_list) self._doc[self._X_APIGW_POLICY] = self.resource_policy @@ -1268,3 +1277,17 @@ def safe_compare_regex_with_string(regex, data): def get_path_without_trailing_slash(path): # convert greedy paths to such as {greedy+}, {proxy+} to "*" return re.sub(r"{([a-zA-Z0-9._-]+|[a-zA-Z0-9._-]+\+|proxy\+)}", "*", path) + + @staticmethod + def _validate_list_property_is_resolved(property_list): + """ + Validate if the values of a Property List are all of type string + + :param property_list: Value of a Property List + :return bool: True if the property_list is all of type string otherwise False + """ + + if property_list is not None and not all(isinstance(x, string_types) for x in property_list): + return False + + return True diff --git a/tests/swagger/test_swagger.py b/tests/swagger/test_swagger.py index fa3f8e580..e6381d526 100644 --- a/tests/swagger/test_swagger.py +++ b/tests/swagger/test_swagger.py @@ -1181,6 +1181,18 @@ def test_must_add_vpc_allow_string_only(self): self.assertEqual(deep_sort_lists(expected), deep_sort_lists(self.editor.swagger[_X_POLICY])) + @parameterized.expand( + [ + param("SourceVpcWhitelist"), + param("SourceVpcBlacklist"), + ] + ) + def test_must_fail_when_vpc_whitelist_is_non_string(self, resource_policy_key): + resource_policy = {resource_policy_key: [{"sub": "somevalue"}]} + + with self.assertRaises(InvalidDocumentException): + self.editor.add_resource_policy(resource_policy, "/foo", "123", "prod") + def test_must_add_vpc_deny_string_only(self): resourcePolicy = { diff --git a/tests/translator/input/error_api_invalid_source_vpc_blacklist.yaml b/tests/translator/input/error_api_invalid_source_vpc_blacklist.yaml new file mode 100644 index 000000000..27eadd1fe --- /dev/null +++ b/tests/translator/input/error_api_invalid_source_vpc_blacklist.yaml @@ -0,0 +1,31 @@ +Globals: + Api: + Auth: + ResourcePolicy: + SourceVpcBlacklist: [{"Ref":"SomeParameter"}] + +Parameters: + SomeParameter: + Type: String + Default: param + +Resources: + MyFunction: + Type: AWS::Serverless::Function + Properties: + InlineCode: | + exports.handler = async (event) => { + const response = { + statusCode: 200, + body: JSON.stringify('Hello from Lambda!'), + }; + return response; + }; + Handler: index.handler + Runtime: nodejs12.x + Events: + Api: + Type: Api + Properties: + Method: Put + Path: /get \ No newline at end of file diff --git a/tests/translator/input/error_api_invalid_source_vpc_whitelist.yaml b/tests/translator/input/error_api_invalid_source_vpc_whitelist.yaml new file mode 100644 index 000000000..99ba49f2d --- /dev/null +++ b/tests/translator/input/error_api_invalid_source_vpc_whitelist.yaml @@ -0,0 +1,31 @@ +Globals: + Api: + Auth: + ResourcePolicy: + SourceVpcWhitelist: [{"Ref":"SomeParameter"}] + +Parameters: + SomeParameter: + Type: String + Default: param + +Resources: + MyFunction: + Type: AWS::Serverless::Function + Properties: + InlineCode: | + exports.handler = async (event) => { + const response = { + statusCode: 200, + body: JSON.stringify('Hello from Lambda!'), + }; + return response; + }; + Handler: index.handler + Runtime: nodejs12.x + Events: + Api: + Type: Api + Properties: + Method: Put + Path: /get \ No newline at end of file diff --git a/tests/translator/output/error_api_invalid_source_vpc_blacklist.json b/tests/translator/output/error_api_invalid_source_vpc_blacklist.json new file mode 100644 index 000000000..cc7d2188a --- /dev/null +++ b/tests/translator/output/error_api_invalid_source_vpc_blacklist.json @@ -0,0 +1,3 @@ +{ + "errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 1. SourceVpcBlacklist must be a list of strings. Use IntrinsicVpcBlacklist instead for values that use Intrinsic Functions" +} diff --git a/tests/translator/output/error_api_invalid_source_vpc_whitelist.json b/tests/translator/output/error_api_invalid_source_vpc_whitelist.json new file mode 100644 index 000000000..5ce6cf3fd --- /dev/null +++ b/tests/translator/output/error_api_invalid_source_vpc_whitelist.json @@ -0,0 +1,3 @@ +{ + "errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 1. SourceVpcWhitelist must be a list of strings. Use IntrinsicVpcWhitelist instead for values that use Intrinsic Functions" +} From 367f2dbda8278267d10ab93f976a1d27a881efba Mon Sep 17 00:00:00 2001 From: Qingchuan Ma <69653965+qingchm@users.noreply.github.com> Date: Mon, 10 May 2021 16:46:45 -0700 Subject: [PATCH 15/16] chore: bump version to 1.36.0 (#2014) --- samtranslator/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samtranslator/__init__.py b/samtranslator/__init__.py index cf51ab459..4a1dc255b 100644 --- a/samtranslator/__init__.py +++ b/samtranslator/__init__.py @@ -1 +1 @@ -__version__ = "1.35.0" +__version__ = "1.36.0" From af57310cb523fc153f9c103b5090d6446f6abd9e Mon Sep 17 00:00:00 2001 From: Jacob Fuss <32497805+jfuss@users.noreply.github.com> Date: Thu, 13 May 2021 17:03:28 -0500 Subject: [PATCH 16/16] Revert "fix: Crash when using an invalid method in open api (#2001)" (#2021) This reverts commit d57b1329e1b99169f2d5ca59c1c41eee66b33750. --- samtranslator/swagger/swagger.py | 13 ----- tests/swagger/test_swagger.py | 36 -------------- .../error_api_with_invalid_path_object.yaml | 47 ------------------- .../error_invalid_method_definition.yaml | 2 +- .../error_api_with_invalid_path_object.json | 3 -- tests/translator/test_translator.py | 1 + 6 files changed, 2 insertions(+), 100 deletions(-) delete mode 100644 tests/translator/input/error_api_with_invalid_path_object.yaml delete mode 100644 tests/translator/output/error_api_with_invalid_path_object.json diff --git a/samtranslator/swagger/swagger.py b/samtranslator/swagger/swagger.py index 36c9f5d18..53ac00bee 100644 --- a/samtranslator/swagger/swagger.py +++ b/samtranslator/swagger/swagger.py @@ -531,19 +531,6 @@ def set_path_default_authorizer( if add_default_auth_to_preflight or normalized_method_name != "options": normalized_method_name = self._normalize_method_name(method_name) # It is possible that the method could have two definitions in a Fn::If block. - - # check for valid methods - if normalized_method_name.upper() not in self._ALL_HTTP_METHODS: - raise InvalidDocumentException( - [ - InvalidTemplateException( - "Path '{}' contains method '{}' which is not a supported method {}".format( - path, method_name, self._ALL_HTTP_METHODS - ) - ) - ] - ) - for method_definition in self.get_method_contents(self.get_path(path)[normalized_method_name]): # If no integration given, then we don't need to process this definition (could be AWS::NoValue) diff --git a/tests/swagger/test_swagger.py b/tests/swagger/test_swagger.py index e6381d526..f237aa103 100644 --- a/tests/swagger/test_swagger.py +++ b/tests/swagger/test_swagger.py @@ -1468,39 +1468,3 @@ def test_should_include_none_if_default_is_overwritte(self): self.editor.add_auth_to_method("/cognito", "get", auth, self.api) self.assertEqual([{"NONE": []}], self.editor.swagger["paths"]["/cognito"]["get"]["security"]) - - -class TestSwaggerEditor_set_path_default_authorizer(TestCase): - def setUp(self): - self.api = api = { - "Auth": { - "Authorizers": {"MyOtherCognitoAuth": {}, "MyCognitoAuth": {}}, - "DefaultAuthorizer": "MyCognitoAuth", - } - } - self.editor = SwaggerEditor( - { - "swagger": "2.0", - "paths": { - "/cognito": { - "nonMethod": { - "x-amazon-apigateway-integration": { - "httpMethod": "POST", - "type": "aws_proxy", - "uri": { - "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${MyFn.Arn}/invocations" - }, - }, - "security": [], - "responses": {}, - } - } - }, - } - ) - - def test_should_fail_when_path_methods_are_invalid(self): - with self.assertRaises(InvalidDocumentException): - self.editor.set_path_default_authorizer( - "/cognito", "MyCognitoAuth", {"MyOtherCognitoAuth": {}, "MyCognitoAuth": {}} - ) diff --git a/tests/translator/input/error_api_with_invalid_path_object.yaml b/tests/translator/input/error_api_with_invalid_path_object.yaml deleted file mode 100644 index 3b51d2dc5..000000000 --- a/tests/translator/input/error_api_with_invalid_path_object.yaml +++ /dev/null @@ -1,47 +0,0 @@ -Globals: - Api: - Name: "some api" - Variables: - SomeVar: Value - Auth: - DefaultAuthorizer: MyCognitoAuth - Authorizers: - MyCognitoAuth: - UserPoolArn: !GetAtt MyUserPool.Arn - -Resources: - ImplicitApiFunction: - Type: AWS::Serverless::Function - Properties: - CodeUri: s3://sam-demo-bucket/member_portal.zip - Handler: index.gethtml - Runtime: nodejs12.x - - ExplicitApi: - Type: AWS::Serverless::Api - Properties: - StageName: SomeStage - DefinitionBody: - swagger: 2.0 - paths: - "/a": - SomeInvalidKey: - x-amazon-apigateway-integration: - httpMethod: POST - type: aws_proxy - uri: !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${ImplicitApiFunction.Arn}/invocations - responses: {} - - MyUserPool: - Type: AWS::Cognito::UserPool - Properties: - UserPoolName: UserPoolName - Policies: - PasswordPolicy: - MinimumLength: 8 - UsernameAttributes: - - email - Schema: - - AttributeDataType: String - Name: email - Required: false \ No newline at end of file diff --git a/tests/translator/input/error_invalid_method_definition.yaml b/tests/translator/input/error_invalid_method_definition.yaml index 440ffca7d..fd9b6617f 100644 --- a/tests/translator/input/error_invalid_method_definition.yaml +++ b/tests/translator/input/error_invalid_method_definition.yaml @@ -42,7 +42,7 @@ Resources: description: Application domain type: string required: true - options: + tags: - InvalidMethodDefinition get: x-amazon-apigateway-integration: diff --git a/tests/translator/output/error_api_with_invalid_path_object.json b/tests/translator/output/error_api_with_invalid_path_object.json deleted file mode 100644 index 03e125d85..000000000 --- a/tests/translator/output/error_api_with_invalid_path_object.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "errorMessage": "Invalid Serverless Application Specification document. Number of errors found: 1. Path '/a' contains method 'SomeInvalidKey' which is not a supported method ['OPTIONS', 'GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'PATCH']" -} \ No newline at end of file diff --git a/tests/translator/test_translator.py b/tests/translator/test_translator.py index 9bbe916e5..5a100fe38 100644 --- a/tests/translator/test_translator.py +++ b/tests/translator/test_translator.py @@ -498,6 +498,7 @@ def test_transform_success(self, testcase, partition_with_region): ], # Run all the above tests against each of the list of partitions to test against ) ) + @pytest.mark.slow @patch( "samtranslator.plugins.application.serverless_app_plugin.ServerlessAppPlugin._sar_service_call", mock_sar_service_call,