diff --git a/integration/combination/test_function_with_http_api.py b/integration/combination/test_function_with_http_api.py index a2741fd46..12c98301d 100644 --- a/integration/combination/test_function_with_http_api.py +++ b/integration/combination/test_function_with_http_api.py @@ -21,3 +21,12 @@ def test_function_with_http_api(self): self.verify_get_request_response(base_url + "some/path", 200) self.verify_get_request_response(base_url + "something", 404) self.verify_get_request_response(base_url + "another/endpoint", 404) + + def test_function_with_http_api_default_path(self): + self.create_and_verify_stack("combination/function_with_http_api_default_path") + + stack_outputs = self.get_stack_outputs() + base_url = stack_outputs["ApiUrl"] + # The $default route catches requests that don't explicitly match other routes + self.verify_get_request_response(base_url, 200) + self.verify_get_request_response(base_url + "something", 200) diff --git a/integration/resources/expected/combination/function_with_http_api_default_path.json b/integration/resources/expected/combination/function_with_http_api_default_path.json new file mode 100644 index 000000000..7f6ef2926 --- /dev/null +++ b/integration/resources/expected/combination/function_with_http_api_default_path.json @@ -0,0 +1,7 @@ +[ + { "LogicalResourceId":"MyLambdaFunction", "ResourceType":"AWS::Lambda::Function" }, + { "LogicalResourceId":"MyLambdaFunctionRole", "ResourceType":"AWS::IAM::Role" }, + { "LogicalResourceId":"MyLambdaFunctionGetApiPermission", "ResourceType":"AWS::Lambda::Permission" }, + { "LogicalResourceId":"MyApi", "ResourceType":"AWS::ApiGatewayV2::Api" }, + { "LogicalResourceId":"MyApiApiGatewayDefaultStage", "ResourceType":"AWS::ApiGatewayV2::Stage" } +] \ No newline at end of file diff --git a/integration/resources/templates/combination/function_with_http_api_default_path.yaml b/integration/resources/templates/combination/function_with_http_api_default_path.yaml new file mode 100644 index 000000000..5f8e903bc --- /dev/null +++ b/integration/resources/templates/combination/function_with_http_api_default_path.yaml @@ -0,0 +1,39 @@ +Resources: + MyLambdaFunction: + Type: AWS::Serverless::Function + Properties: + Handler: index.handler + Runtime: python3.7 + InlineCode: | + def handler(event, context): + return {'body': 'Hello World!', 'statusCode': 200} + MemorySize: 128 + Events: + GetApi: + Type: HttpApi + Properties: + ApiId: + Ref: MyApi + Method: ANY + Path: /$default + + MyApi: + Type: AWS::Serverless::HttpApi + Properties: + DefinitionBody: + info: + version: '1.0' + title: + Ref: AWS::StackName + paths: + /$default: + x-amazon-apigateway-any-method: + responses: { } + isDefaultRoute: true + openapi: 3.0.1 + +Outputs: + ApiUrl: + Description: "API endpoint URL for Prod environment" + Value: + Fn::Sub: "https://${MyApi}.execute-api.${AWS::Region}.${AWS::URLSuffix}/" \ No newline at end of file diff --git a/samtranslator/model/eventsources/push.py b/samtranslator/model/eventsources/push.py index b375e3f71..b3f9a7177 100644 --- a/samtranslator/model/eventsources/push.py +++ b/samtranslator/model/eventsources/push.py @@ -1173,8 +1173,16 @@ def _get_permission(self, resources_to_link, stage): # type: ignore[no-untyped- api_id = self.ApiId # type: ignore[attr-defined] + # when the Method is "ANY" and the path is '/$default' it adds an extra "*" which causes a bug + # the generated ARN for permissions ends with /*/*/$default which causes the path to be invalid + # see this issue: https://github.com/aws/serverless-application-model/issues/1860 + resource = "${__ApiId__}/${__Stage__}" + if self.Method.lower() == "any" and path == f"/{OpenApiEditor._DEFAULT_PATH}": + resource += path + else: + resource += f"/{method}{path}" + # ApiId can be a simple string or intrinsic function like !Ref. Using Fn::Sub will handle both cases - resource = "${__ApiId__}/" + "${__Stage__}/" + method + path source_arn = fnSub( ArnGenerator.generate_arn(partition="${AWS::Partition}", service="execute-api", resource=resource), # type: ignore[no-untyped-call] {"__ApiId__": api_id, "__Stage__": stage}, diff --git a/tests/translator/input/explicit_http_api_default_path.yaml b/tests/translator/input/explicit_http_api_default_path.yaml new file mode 100644 index 000000000..e45a0ade4 --- /dev/null +++ b/tests/translator/input/explicit_http_api_default_path.yaml @@ -0,0 +1,45 @@ +Transform: AWS::Serverless-2016-10-31 +Resources: + Function: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://sam-demo-bucket/todo_list.zip + Handler: index.restapi + Runtime: python3.7 + Events: + HttpApiANYdefault: + Type: HttpApi + Properties: + Path: /$default + Method: ANY + ApiId: !Ref HttpApi + Function2: + Type: AWS::Serverless::Function + Properties: + CodeUri: s3://sam-demo-bucket/todo_list.zip + Handler: index.restapi + Runtime: python3.7 + Events: + HttpApiANYhello: + Type: HttpApi + Properties: + Path: /hello + Method: ANY + ApiId: !Ref HttpApi + HttpApi: + Type: AWS::Serverless::HttpApi + Properties: + DefinitionBody: + openapi: '3.0' + info: + title: !Sub ${AWS::StackName}-HttpApi + version: '1.0' + paths: + /$default: + x-amazon-apigateway-any-method: + responses: {} + isDefaultRoute: true + /hello: + x-amazon-apigateway-any-method: + responses: {} + FailOnWarnings: true \ No newline at end of file diff --git a/tests/translator/output/aws-cn/explicit_http_api_default_path.json b/tests/translator/output/aws-cn/explicit_http_api_default_path.json new file mode 100644 index 000000000..38bbdbc07 --- /dev/null +++ b/tests/translator/output/aws-cn/explicit_http_api_default_path.json @@ -0,0 +1,215 @@ +{ + "Resources": { + "Function": { + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "todo_list.zip" + }, + "Handler": "index.restapi", + "Role": { + "Fn::GetAtt": [ + "FunctionRole", + "Arn" + ] + }, + "Runtime": "python3.7", + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + }, + "Type": "AWS::Lambda::Function" + }, + "Function2": { + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "todo_list.zip" + }, + "Handler": "index.restapi", + "Role": { + "Fn::GetAtt": [ + "Function2Role", + "Arn" + ] + }, + "Runtime": "python3.7", + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + }, + "Type": "AWS::Lambda::Function" + }, + "Function2HttpApiANYhelloPermission": { + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Ref": "Function2" + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Sub": [ + "arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${__ApiId__}/${__Stage__}/*/hello", + { + "__ApiId__": { + "Ref": "HttpApi" + }, + "__Stage__": "*" + } + ] + } + }, + "Type": "AWS::Lambda::Permission" + }, + "Function2Role": { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + "arn:aws-cn:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + }, + "Type": "AWS::IAM::Role" + }, + "FunctionHttpApiANYdefaultPermission": { + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Ref": "Function" + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Sub": [ + "arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${__ApiId__}/${__Stage__}/$default", + { + "__ApiId__": { + "Ref": "HttpApi" + }, + "__Stage__": "*" + } + ] + } + }, + "Type": "AWS::Lambda::Permission" + }, + "FunctionRole": { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + "arn:aws-cn:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + }, + "Type": "AWS::IAM::Role" + }, + "HttpApi": { + "Properties": { + "Body": { + "info": { + "title": { + "Fn::Sub": "${AWS::StackName}-HttpApi" + }, + "version": "1.0" + }, + "openapi": "3.0", + "paths": { + "/$default": { + "x-amazon-apigateway-any-method": { + "isDefaultRoute": true, + "responses": {}, + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "payloadFormatVersion": "2.0", + "type": "aws_proxy", + "uri": { + "Fn::Sub": "arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${Function.Arn}/invocations" + } + } + } + }, + "/hello": { + "x-amazon-apigateway-any-method": { + "responses": {}, + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "payloadFormatVersion": "2.0", + "type": "aws_proxy", + "uri": { + "Fn::Sub": "arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${Function2.Arn}/invocations" + } + } + } + } + }, + "tags": [ + { + "name": "httpapi:createdBy", + "x-amazon-apigateway-tag-value": "SAM" + } + ] + }, + "FailOnWarnings": true + }, + "Type": "AWS::ApiGatewayV2::Api" + }, + "HttpApiApiGatewayDefaultStage": { + "Properties": { + "ApiId": { + "Ref": "HttpApi" + }, + "AutoDeploy": true, + "StageName": "$default", + "Tags": { + "httpapi:createdBy": "SAM" + } + }, + "Type": "AWS::ApiGatewayV2::Stage" + } + } +} diff --git a/tests/translator/output/aws-us-gov/explicit_http_api_default_path.json b/tests/translator/output/aws-us-gov/explicit_http_api_default_path.json new file mode 100644 index 000000000..c75091ad3 --- /dev/null +++ b/tests/translator/output/aws-us-gov/explicit_http_api_default_path.json @@ -0,0 +1,215 @@ +{ + "Resources": { + "Function": { + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "todo_list.zip" + }, + "Handler": "index.restapi", + "Role": { + "Fn::GetAtt": [ + "FunctionRole", + "Arn" + ] + }, + "Runtime": "python3.7", + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + }, + "Type": "AWS::Lambda::Function" + }, + "Function2": { + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "todo_list.zip" + }, + "Handler": "index.restapi", + "Role": { + "Fn::GetAtt": [ + "Function2Role", + "Arn" + ] + }, + "Runtime": "python3.7", + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + }, + "Type": "AWS::Lambda::Function" + }, + "Function2HttpApiANYhelloPermission": { + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Ref": "Function2" + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Sub": [ + "arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${__ApiId__}/${__Stage__}/*/hello", + { + "__ApiId__": { + "Ref": "HttpApi" + }, + "__Stage__": "*" + } + ] + } + }, + "Type": "AWS::Lambda::Permission" + }, + "Function2Role": { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + "arn:aws-us-gov:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + }, + "Type": "AWS::IAM::Role" + }, + "FunctionHttpApiANYdefaultPermission": { + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Ref": "Function" + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Sub": [ + "arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${__ApiId__}/${__Stage__}/$default", + { + "__ApiId__": { + "Ref": "HttpApi" + }, + "__Stage__": "*" + } + ] + } + }, + "Type": "AWS::Lambda::Permission" + }, + "FunctionRole": { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + "arn:aws-us-gov:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + }, + "Type": "AWS::IAM::Role" + }, + "HttpApi": { + "Properties": { + "Body": { + "info": { + "title": { + "Fn::Sub": "${AWS::StackName}-HttpApi" + }, + "version": "1.0" + }, + "openapi": "3.0", + "paths": { + "/$default": { + "x-amazon-apigateway-any-method": { + "isDefaultRoute": true, + "responses": {}, + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "payloadFormatVersion": "2.0", + "type": "aws_proxy", + "uri": { + "Fn::Sub": "arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${Function.Arn}/invocations" + } + } + } + }, + "/hello": { + "x-amazon-apigateway-any-method": { + "responses": {}, + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "payloadFormatVersion": "2.0", + "type": "aws_proxy", + "uri": { + "Fn::Sub": "arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${Function2.Arn}/invocations" + } + } + } + } + }, + "tags": [ + { + "name": "httpapi:createdBy", + "x-amazon-apigateway-tag-value": "SAM" + } + ] + }, + "FailOnWarnings": true + }, + "Type": "AWS::ApiGatewayV2::Api" + }, + "HttpApiApiGatewayDefaultStage": { + "Properties": { + "ApiId": { + "Ref": "HttpApi" + }, + "AutoDeploy": true, + "StageName": "$default", + "Tags": { + "httpapi:createdBy": "SAM" + } + }, + "Type": "AWS::ApiGatewayV2::Stage" + } + } +} diff --git a/tests/translator/output/explicit_http_api_default_path.json b/tests/translator/output/explicit_http_api_default_path.json new file mode 100644 index 000000000..09d486776 --- /dev/null +++ b/tests/translator/output/explicit_http_api_default_path.json @@ -0,0 +1,215 @@ +{ + "Resources": { + "Function": { + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "todo_list.zip" + }, + "Handler": "index.restapi", + "Role": { + "Fn::GetAtt": [ + "FunctionRole", + "Arn" + ] + }, + "Runtime": "python3.7", + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + }, + "Type": "AWS::Lambda::Function" + }, + "Function2": { + "Properties": { + "Code": { + "S3Bucket": "sam-demo-bucket", + "S3Key": "todo_list.zip" + }, + "Handler": "index.restapi", + "Role": { + "Fn::GetAtt": [ + "Function2Role", + "Arn" + ] + }, + "Runtime": "python3.7", + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + }, + "Type": "AWS::Lambda::Function" + }, + "Function2HttpApiANYhelloPermission": { + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Ref": "Function2" + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Sub": [ + "arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${__ApiId__}/${__Stage__}/*/hello", + { + "__ApiId__": { + "Ref": "HttpApi" + }, + "__Stage__": "*" + } + ] + } + }, + "Type": "AWS::Lambda::Permission" + }, + "Function2Role": { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + }, + "Type": "AWS::IAM::Role" + }, + "FunctionHttpApiANYdefaultPermission": { + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Ref": "Function" + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Sub": [ + "arn:${AWS::Partition}:execute-api:${AWS::Region}:${AWS::AccountId}:${__ApiId__}/${__Stage__}/$default", + { + "__ApiId__": { + "Ref": "HttpApi" + }, + "__Stage__": "*" + } + ] + } + }, + "Type": "AWS::Lambda::Permission" + }, + "FunctionRole": { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": [ + "sts:AssumeRole" + ], + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ], + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + }, + "Type": "AWS::IAM::Role" + }, + "HttpApi": { + "Properties": { + "Body": { + "info": { + "title": { + "Fn::Sub": "${AWS::StackName}-HttpApi" + }, + "version": "1.0" + }, + "openapi": "3.0", + "paths": { + "/$default": { + "x-amazon-apigateway-any-method": { + "isDefaultRoute": true, + "responses": {}, + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "payloadFormatVersion": "2.0", + "type": "aws_proxy", + "uri": { + "Fn::Sub": "arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${Function.Arn}/invocations" + } + } + } + }, + "/hello": { + "x-amazon-apigateway-any-method": { + "responses": {}, + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "payloadFormatVersion": "2.0", + "type": "aws_proxy", + "uri": { + "Fn::Sub": "arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${Function2.Arn}/invocations" + } + } + } + } + }, + "tags": [ + { + "name": "httpapi:createdBy", + "x-amazon-apigateway-tag-value": "SAM" + } + ] + }, + "FailOnWarnings": true + }, + "Type": "AWS::ApiGatewayV2::Api" + }, + "HttpApiApiGatewayDefaultStage": { + "Properties": { + "ApiId": { + "Ref": "HttpApi" + }, + "AutoDeploy": true, + "StageName": "$default", + "Tags": { + "httpapi:createdBy": "SAM" + } + }, + "Type": "AWS::ApiGatewayV2::Stage" + } + } +}