From d4e5922ad0f4494a61f9fb255623abf91fa55757 Mon Sep 17 00:00:00 2001 From: connorrobertson Date: Tue, 28 Feb 2023 14:32:37 -0800 Subject: [PATCH 01/10] Cors Fix --- .../internal/schema_source/aws_serverless_api.py | 1 + samtranslator/model/api/api_generator.py | 9 +++++---- samtranslator/schema/schema.json | 4 ++++ samtranslator/swagger/swagger.py | 11 ++++++++++- schema_source/sam.schema.json | 4 ++++ 5 files changed, 24 insertions(+), 5 deletions(-) diff --git a/samtranslator/internal/schema_source/aws_serverless_api.py b/samtranslator/internal/schema_source/aws_serverless_api.py index 5e3965853..f07a9b407 100644 --- a/samtranslator/internal/schema_source/aws_serverless_api.py +++ b/samtranslator/internal/schema_source/aws_serverless_api.py @@ -100,6 +100,7 @@ class UsagePlan(BaseModel): class Auth(BaseModel): AddDefaultAuthorizerToCorsPreflight: Optional[bool] = auth("AddDefaultAuthorizerToCorsPreflight") + AddApiKeyRequiredToCorsPreflight: Optional[bool] # TODO Add Docs ApiKeyRequired: Optional[bool] = auth("ApiKeyRequired") Authorizers: Optional[ Dict[ diff --git a/samtranslator/model/api/api_generator.py b/samtranslator/model/api/api_generator.py index 23972e336..bc66b0c16 100644 --- a/samtranslator/model/api/api_generator.py +++ b/samtranslator/model/api/api_generator.py @@ -52,12 +52,13 @@ "DefaultAuthorizer", "InvokeRole", "AddDefaultAuthorizerToCorsPreflight", + "AddApiKeyRequiredToCorsPreflight", "ApiKeyRequired", "ResourcePolicy", "UsagePlan", ], ) -AuthProperties.__new__.__defaults__ = (None, None, None, True, None, None, None) +AuthProperties.__new__.__defaults__ = (None, None, None, True, True, None, None, None) UsagePlanProperties = namedtuple( "UsagePlanProperties", ["CreateUsagePlan", "Description", "Quota", "Tags", "Throttle", "UsagePlanName"] ) @@ -745,7 +746,7 @@ def _add_auth(self) -> None: if auth_properties.ApiKeyRequired: swagger_editor.add_apikey_security_definition() - self._set_default_apikey_required(swagger_editor) + self._set_default_apikey_required(swagger_editor, auth_properties.AddApiKeyRequiredToCorsPreflight) if auth_properties.ResourcePolicy: SwaggerEditor.validate_is_dict( @@ -1217,9 +1218,9 @@ def _set_default_authorizer( add_default_auth_to_preflight=add_default_auth_to_preflight, ) - def _set_default_apikey_required(self, swagger_editor: SwaggerEditor) -> None: + def _set_default_apikey_required(self, swagger_editor: SwaggerEditor, AddApiKeyRequiredToCorsPreflight: bool) -> None: for path in swagger_editor.iter_on_path(): - swagger_editor.set_path_default_apikey_required(path) + swagger_editor.set_path_default_apikey_required(path, AddApiKeyRequiredToCorsPreflight) def _set_endpoint_configuration(self, rest_api: ApiGatewayRestApi, value: Union[str, Dict[str, Any]]) -> None: """ diff --git a/samtranslator/schema/schema.json b/samtranslator/schema/schema.json index 72b887778..b6bc2231b 100644 --- a/samtranslator/schema/schema.json +++ b/samtranslator/schema/schema.json @@ -196530,6 +196530,10 @@ "samtranslator__internal__schema_source__aws_serverless_api__Auth": { "additionalProperties": false, "properties": { + "AddApiKeyRequiredToCorsPreflight": { + "title": "Addapikeyrequiredtocorspreflight", + "type": "boolean" + }, "AddDefaultAuthorizerToCorsPreflight": { "description": "If the `DefaultAuthorizer` and `Cors` properties are set, then setting `AddDefaultAuthorizerToCorsPreflight` will cause the default authorizer to be added to the `Options` property in the OpenAPI section\\. \n*Type*: Boolean \n*Required*: No \n*Default*: True \n*AWS CloudFormation compatibility*: This property is unique to AWS SAM and doesn't have an AWS CloudFormation equivalent\\.", "markdownDescription": "If the `DefaultAuthorizer` and `Cors` properties are set, then setting `AddDefaultAuthorizerToCorsPreflight` will cause the default authorizer to be added to the `Options` property in the OpenAPI section\\. \n*Type*: Boolean \n*Required*: No \n*Default*: True \n*AWS CloudFormation compatibility*: This property is unique to AWS SAM and doesn't have an AWS CloudFormation equivalent\\.", diff --git a/samtranslator/swagger/swagger.py b/samtranslator/swagger/swagger.py index d5a74909d..06ce7d97d 100644 --- a/samtranslator/swagger/swagger.py +++ b/samtranslator/swagger/swagger.py @@ -612,7 +612,7 @@ def set_path_default_authorizer( # noqa: too-many-branches if "AWS_IAM" in method_definition["security"][0]: self.add_awsiam_security_definition() - def set_path_default_apikey_required(self, path: str) -> None: + def set_path_default_apikey_required(self, path: str, AddApiKeyRequiredToCorsPreflight=True) -> None: """ Add the ApiKey security as required for each method on this path unless ApiKeyRequired was defined at the Function/Path/Method level. This is intended to be used to set the @@ -620,6 +620,8 @@ def set_path_default_apikey_required(self, path: str) -> None: Serverless API. :param string path: Path name + :param bool AddApiKeyRequiredToCorsPreflight: Bool of whether to add the ApiKeyRequired + to OPTIONS preflight requests. """ for method_name, method_definition in self.iter_on_all_methods_for_path(path): # type: ignore[no-untyped-call] @@ -673,6 +675,9 @@ def set_path_default_apikey_required(self, path: str) -> None: security = existing_non_apikey_security + apikey_security + if method_name == "options" and not AddApiKeyRequiredToCorsPreflight: + security = existing_non_apikey_security + if security != existing_security: method_definition["security"] = security @@ -695,6 +700,10 @@ def add_auth_to_method(self, path: str, method_name: str, auth: Dict[str, Any], self._set_method_authorizer(path, method_name, method_authorizer, authorizers, method_scopes) # type: ignore[no-untyped-call] method_apikey_required = auth and auth.get("ApiKeyRequired") + + if auth.get("AddApiKeyRequiredToCorsPreflight") and method_name == "options": + method_apikey_required = False + if method_apikey_required is not None: self._set_method_apikey_handling(path, method_name, method_apikey_required) # type: ignore[no-untyped-call] diff --git a/schema_source/sam.schema.json b/schema_source/sam.schema.json index 362e46c14..d84cb32cc 100644 --- a/schema_source/sam.schema.json +++ b/schema_source/sam.schema.json @@ -2929,6 +2929,10 @@ "samtranslator__internal__schema_source__aws_serverless_api__Auth": { "additionalProperties": false, "properties": { + "AddApiKeyRequiredToCorsPreflight": { + "title": "Addapikeyrequiredtocorspreflight", + "type": "boolean" + }, "AddDefaultAuthorizerToCorsPreflight": { "description": "If the `DefaultAuthorizer` and `Cors` properties are set, then setting `AddDefaultAuthorizerToCorsPreflight` will cause the default authorizer to be added to the `Options` property in the OpenAPI section\\. \n*Type*: Boolean \n*Required*: No \n*Default*: True \n*AWS CloudFormation compatibility*: This property is unique to AWS SAM and doesn't have an AWS CloudFormation equivalent\\.", "markdownDescription": "If the `DefaultAuthorizer` and `Cors` properties are set, then setting `AddDefaultAuthorizerToCorsPreflight` will cause the default authorizer to be added to the `Options` property in the OpenAPI section\\. \n*Type*: Boolean \n*Required*: No \n*Default*: True \n*AWS CloudFormation compatibility*: This property is unique to AWS SAM and doesn't have an AWS CloudFormation equivalent\\.", From 1bb436d2365e08031f4cd94c9b362319651e7920 Mon Sep 17 00:00:00 2001 From: connorrobertson Date: Wed, 1 Mar 2023 17:37:38 -0800 Subject: [PATCH 02/10] Added integ and transform tests --- samtranslator/model/api/api_generator.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/samtranslator/model/api/api_generator.py b/samtranslator/model/api/api_generator.py index bf942001b..d2187e2d4 100644 --- a/samtranslator/model/api/api_generator.py +++ b/samtranslator/model/api/api_generator.py @@ -1225,7 +1225,9 @@ def _set_default_authorizer( add_default_auth_to_preflight=add_default_auth_to_preflight, ) - def _set_default_apikey_required(self, swagger_editor: SwaggerEditor, AddApiKeyRequiredToCorsPreflight: bool) -> None: + def _set_default_apikey_required( + self, swagger_editor: SwaggerEditor, AddApiKeyRequiredToCorsPreflight: bool + ) -> None: for path in swagger_editor.iter_on_path(): swagger_editor.set_path_default_apikey_required(path, AddApiKeyRequiredToCorsPreflight) From 4cdf1ba6a717973cb7f5b8c3f19fd15606a5e6d5 Mon Sep 17 00:00:00 2001 From: connorrobertson Date: Wed, 1 Mar 2023 17:39:53 -0800 Subject: [PATCH 03/10] Add integ + transform tests for real this time --- integration/combination/test_api_with_cors.py | 5 +- .../combination/api_with_cors_and_apikey.json | 26 ++ .../combination/api_with_cors_and_apikey.yaml | 87 +++++ samtranslator/model/api/api_generator.py | 13 +- samtranslator/swagger/swagger.py | 2 +- .../input/api_with_cors_and_apikey.yaml | 87 +++++ .../output/api_with_cors_and_apikey.json | 297 +++++++++++++++++ .../aws-cn/api_with_cors_and_apikey.json | 305 ++++++++++++++++++ .../aws-us-gov/api_with_cors_and_apikey.json | 305 ++++++++++++++++++ 9 files changed, 1111 insertions(+), 16 deletions(-) create mode 100644 integration/resources/expected/combination/api_with_cors_and_apikey.json create mode 100644 integration/resources/templates/combination/api_with_cors_and_apikey.yaml create mode 100644 tests/translator/input/api_with_cors_and_apikey.yaml create mode 100644 tests/translator/output/api_with_cors_and_apikey.json create mode 100644 tests/translator/output/aws-cn/api_with_cors_and_apikey.json create mode 100644 tests/translator/output/aws-us-gov/api_with_cors_and_apikey.json diff --git a/integration/combination/test_api_with_cors.py b/integration/combination/test_api_with_cors.py index 55fab2777..5906e9d89 100644 --- a/integration/combination/test_api_with_cors.py +++ b/integration/combination/test_api_with_cors.py @@ -12,10 +12,7 @@ @skipIf(current_region_does_not_support([REST_API]), "Rest API is not supported in this testing region") class TestApiWithCors(BaseTest): @parameterized.expand( - [ - "combination/api_with_cors", - "combination/api_with_cors_openapi", - ] + ["combination/api_with_cors", "combination/api_with_cors_openapi", "combination/api_with_cors_and_apikey"] ) def test_cors(self, file_name): self.create_and_verify_stack(file_name) diff --git a/integration/resources/expected/combination/api_with_cors_and_apikey.json b/integration/resources/expected/combination/api_with_cors_and_apikey.json new file mode 100644 index 000000000..503bbd9b7 --- /dev/null +++ b/integration/resources/expected/combination/api_with_cors_and_apikey.json @@ -0,0 +1,26 @@ +[ + { + "LogicalResourceId": "MyApi", + "ResourceType": "AWS::ApiGateway::RestApi" + }, + { + "LogicalResourceId": "MyApiDeployment", + "ResourceType": "AWS::ApiGateway::Deployment" + }, + { + "LogicalResourceId": "MyApidevStage", + "ResourceType": "AWS::ApiGateway::Stage" + }, + { + "LogicalResourceId": "ApiGatewayLambdaRole", + "ResourceType": "AWS::IAM::Role" + }, + { + "LogicalResourceId": "MyFunction", + "ResourceType": "AWS::Lambda::Function" + }, + { + "LogicalResourceId": "MyFunctionRole", + "ResourceType": "AWS::IAM::Role" + } +] diff --git a/integration/resources/templates/combination/api_with_cors_and_apikey.yaml b/integration/resources/templates/combination/api_with_cors_and_apikey.yaml new file mode 100644 index 000000000..d005a1429 --- /dev/null +++ b/integration/resources/templates/combination/api_with_cors_and_apikey.yaml @@ -0,0 +1,87 @@ +AWSTemplateFormatVersion: '2010-09-09' + +Transform: +- AWS::Serverless-2016-10-31 + +Globals: + Api: + Auth: + ApiKeyRequired: true + AddApiKeyRequiredToCorsPreflight: false + +Resources: + + MyFunction: + Type: AWS::Serverless::Function + Properties: + Handler: index.handler + InlineCode: | + exports.handler = async function (event) { + return { + statusCode: 200, + body: JSON.stringify({ message: "Hello, SAM!" }), + } + } + Runtime: nodejs16.x + + ApiGatewayLambdaRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Principal: {Service: apigateway.amazonaws.com} + Action: sts:AssumeRole + Policies: + - PolicyName: AllowInvokeLambdaFunctions + PolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Action: lambda:InvokeFunction + Resource: '*' + + MyApi: + Type: AWS::Serverless::Api + Properties: + Cors: + AllowMethods: "'methods'" + AllowHeaders: "'headers'" + AllowOrigin: "'origins'" + MaxAge: "'600'" + Auth: + ApiKeyRequired: true + StageName: dev + DefinitionBody: + openapi: 3.0.1 + paths: + /apione: + get: + x-amazon-apigateway-integration: + credentials: + Fn::Sub: ${ApiGatewayLambdaRole.Arn} + uri: + Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${MyFunction.Arn}/invocations + passthroughBehavior: when_no_match + httpMethod: POST + type: aws_proxy + /apitwo: + get: + x-amazon-apigateway-integration: + credentials: + Fn::Sub: ${ApiGatewayLambdaRole.Arn} + uri: + Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${MyFunction.Arn}/invocations + passthroughBehavior: when_no_match + httpMethod: POST + type: aws_proxy + + + +Outputs: + ApiUrl: + Description: URL of your API endpoint + Value: !Sub "https://${MyApi}.execute-api.${AWS::Region}.amazonaws.com/dev/" +Metadata: + SamTransformTest: true diff --git a/samtranslator/model/api/api_generator.py b/samtranslator/model/api/api_generator.py index d2187e2d4..bc66b0c16 100644 --- a/samtranslator/model/api/api_generator.py +++ b/samtranslator/model/api/api_generator.py @@ -195,7 +195,6 @@ def __init__( # noqa: too-many-arguments description: Optional[Intrinsicable[str]] = None, mode: Optional[Intrinsicable[str]] = None, api_key_source_type: Optional[Intrinsicable[str]] = None, - always_deploy: Optional[bool] = False, ): """Constructs an API Generator class that generates API Gateway resources @@ -251,7 +250,6 @@ def __init__( # noqa: too-many-arguments self.template_conditions = template_conditions self.mode = mode self.api_key_source_type = api_key_source_type - self.always_deploy = always_deploy def _construct_rest_api(self) -> ApiGatewayRestApi: """Constructs and returns the ApiGateway RestApi. @@ -428,12 +426,7 @@ def _construct_stage( if swagger is not None: deployment.make_auto_deployable( - stage, - self.remove_extra_stage, - swagger, - self.domain, - redeploy_restapi_parameters, - self.always_deploy, + stage, self.remove_extra_stage, swagger, self.domain, redeploy_restapi_parameters ) if self.tags is not None: @@ -1225,9 +1218,7 @@ def _set_default_authorizer( add_default_auth_to_preflight=add_default_auth_to_preflight, ) - def _set_default_apikey_required( - self, swagger_editor: SwaggerEditor, AddApiKeyRequiredToCorsPreflight: bool - ) -> None: + def _set_default_apikey_required(self, swagger_editor: SwaggerEditor, AddApiKeyRequiredToCorsPreflight: bool) -> None: for path in swagger_editor.iter_on_path(): swagger_editor.set_path_default_apikey_required(path, AddApiKeyRequiredToCorsPreflight) diff --git a/samtranslator/swagger/swagger.py b/samtranslator/swagger/swagger.py index 06ce7d97d..39cd1ad85 100644 --- a/samtranslator/swagger/swagger.py +++ b/samtranslator/swagger/swagger.py @@ -612,7 +612,7 @@ def set_path_default_authorizer( # noqa: too-many-branches if "AWS_IAM" in method_definition["security"][0]: self.add_awsiam_security_definition() - def set_path_default_apikey_required(self, path: str, AddApiKeyRequiredToCorsPreflight=True) -> None: + def set_path_default_apikey_required(self, path: str, AddApiKeyRequiredToCorsPreflight: bool = True) -> None: """ Add the ApiKey security as required for each method on this path unless ApiKeyRequired was defined at the Function/Path/Method level. This is intended to be used to set the diff --git a/tests/translator/input/api_with_cors_and_apikey.yaml b/tests/translator/input/api_with_cors_and_apikey.yaml new file mode 100644 index 000000000..d005a1429 --- /dev/null +++ b/tests/translator/input/api_with_cors_and_apikey.yaml @@ -0,0 +1,87 @@ +AWSTemplateFormatVersion: '2010-09-09' + +Transform: +- AWS::Serverless-2016-10-31 + +Globals: + Api: + Auth: + ApiKeyRequired: true + AddApiKeyRequiredToCorsPreflight: false + +Resources: + + MyFunction: + Type: AWS::Serverless::Function + Properties: + Handler: index.handler + InlineCode: | + exports.handler = async function (event) { + return { + statusCode: 200, + body: JSON.stringify({ message: "Hello, SAM!" }), + } + } + Runtime: nodejs16.x + + ApiGatewayLambdaRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Principal: {Service: apigateway.amazonaws.com} + Action: sts:AssumeRole + Policies: + - PolicyName: AllowInvokeLambdaFunctions + PolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Action: lambda:InvokeFunction + Resource: '*' + + MyApi: + Type: AWS::Serverless::Api + Properties: + Cors: + AllowMethods: "'methods'" + AllowHeaders: "'headers'" + AllowOrigin: "'origins'" + MaxAge: "'600'" + Auth: + ApiKeyRequired: true + StageName: dev + DefinitionBody: + openapi: 3.0.1 + paths: + /apione: + get: + x-amazon-apigateway-integration: + credentials: + Fn::Sub: ${ApiGatewayLambdaRole.Arn} + uri: + Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${MyFunction.Arn}/invocations + passthroughBehavior: when_no_match + httpMethod: POST + type: aws_proxy + /apitwo: + get: + x-amazon-apigateway-integration: + credentials: + Fn::Sub: ${ApiGatewayLambdaRole.Arn} + uri: + Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${MyFunction.Arn}/invocations + passthroughBehavior: when_no_match + httpMethod: POST + type: aws_proxy + + + +Outputs: + ApiUrl: + Description: URL of your API endpoint + Value: !Sub "https://${MyApi}.execute-api.${AWS::Region}.amazonaws.com/dev/" +Metadata: + SamTransformTest: true diff --git a/tests/translator/output/api_with_cors_and_apikey.json b/tests/translator/output/api_with_cors_and_apikey.json new file mode 100644 index 000000000..5f878d55c --- /dev/null +++ b/tests/translator/output/api_with_cors_and_apikey.json @@ -0,0 +1,297 @@ +{ + "AWSTemplateFormatVersion": "2010-09-09", + "Metadata": { + "SamTransformTest": true + }, + "Outputs": { + "ApiUrl": { + "Description": "URL of your API endpoint", + "Value": { + "Fn::Sub": "https://${MyApi}.execute-api.${AWS::Region}.amazonaws.com/dev/" + } + } + }, + "Resources": { + "ApiGatewayLambdaRole": { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "apigateway.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": "lambda:InvokeFunction", + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "AllowInvokeLambdaFunctions" + } + ] + }, + "Type": "AWS::IAM::Role" + }, + "MyApi": { + "Properties": { + "Body": { + "components": { + "securitySchemes": { + "api_key": { + "in": "header", + "name": "x-api-key", + "type": "apiKey" + } + } + }, + "openapi": "3.0.1", + "paths": { + "/apione": { + "get": { + "security": [ + { + "api_key": [] + } + ], + "x-amazon-apigateway-integration": { + "credentials": { + "Fn::Sub": "${ApiGatewayLambdaRole.Arn}" + }, + "httpMethod": "POST", + "passthroughBehavior": "when_no_match", + "type": "aws_proxy", + "uri": { + "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${MyFunction.Arn}/invocations" + } + } + }, + "options": { + "responses": { + "200": { + "description": "Default response for CORS method", + "headers": { + "Access-Control-Allow-Headers": { + "schema": { + "schema": { + "type": "string" + } + } + }, + "Access-Control-Allow-Methods": { + "schema": { + "schema": { + "type": "string" + } + } + }, + "Access-Control-Allow-Origin": { + "schema": { + "schema": { + "type": "string" + } + } + }, + "Access-Control-Max-Age": { + "schema": { + "schema": { + "type": "integer" + } + } + } + } + } + }, + "summary": "CORS support", + "x-amazon-apigateway-integration": { + "requestTemplates": { + "application/json": "{\n \"statusCode\" : 200\n}\n" + }, + "responses": { + "default": { + "responseParameters": { + "method.response.header.Access-Control-Allow-Headers": "'headers'", + "method.response.header.Access-Control-Allow-Methods": "'methods'", + "method.response.header.Access-Control-Allow-Origin": "'origins'", + "method.response.header.Access-Control-Max-Age": "'600'" + }, + "responseTemplates": { + "application/json": "{}\n" + }, + "statusCode": "200" + } + }, + "type": "mock" + } + } + }, + "/apitwo": { + "get": { + "security": [ + { + "api_key": [] + } + ], + "x-amazon-apigateway-integration": { + "credentials": { + "Fn::Sub": "${ApiGatewayLambdaRole.Arn}" + }, + "httpMethod": "POST", + "passthroughBehavior": "when_no_match", + "type": "aws_proxy", + "uri": { + "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${MyFunction.Arn}/invocations" + } + } + }, + "options": { + "responses": { + "200": { + "description": "Default response for CORS method", + "headers": { + "Access-Control-Allow-Headers": { + "schema": { + "schema": { + "type": "string" + } + } + }, + "Access-Control-Allow-Methods": { + "schema": { + "schema": { + "type": "string" + } + } + }, + "Access-Control-Allow-Origin": { + "schema": { + "schema": { + "type": "string" + } + } + }, + "Access-Control-Max-Age": { + "schema": { + "schema": { + "type": "integer" + } + } + } + } + } + }, + "summary": "CORS support", + "x-amazon-apigateway-integration": { + "requestTemplates": { + "application/json": "{\n \"statusCode\" : 200\n}\n" + }, + "responses": { + "default": { + "responseParameters": { + "method.response.header.Access-Control-Allow-Headers": "'headers'", + "method.response.header.Access-Control-Allow-Methods": "'methods'", + "method.response.header.Access-Control-Allow-Origin": "'origins'", + "method.response.header.Access-Control-Max-Age": "'600'" + }, + "responseTemplates": { + "application/json": "{}\n" + }, + "statusCode": "200" + } + }, + "type": "mock" + } + } + } + } + } + }, + "Type": "AWS::ApiGateway::RestApi" + }, + "MyApiDeployment996e54a322": { + "Properties": { + "Description": "RestApi deployment id: 996e54a3224f4396bddbafc7ca104cb3685eccdd", + "RestApiId": { + "Ref": "MyApi" + }, + "StageName": "Stage" + }, + "Type": "AWS::ApiGateway::Deployment" + }, + "MyApidevStage": { + "Properties": { + "DeploymentId": { + "Ref": "MyApiDeployment996e54a322" + }, + "RestApiId": { + "Ref": "MyApi" + }, + "StageName": "dev" + }, + "Type": "AWS::ApiGateway::Stage" + }, + "MyFunction": { + "Properties": { + "Code": { + "ZipFile": "exports.handler = async function (event) {\n return {\n statusCode: 200,\n body: JSON.stringify({ message: \"Hello, SAM!\" }),\n }\n}\n" + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "MyFunctionRole", + "Arn" + ] + }, + "Runtime": "nodejs16.x", + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + }, + "Type": "AWS::Lambda::Function" + }, + "MyFunctionRole": { + "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" + } + } +} diff --git a/tests/translator/output/aws-cn/api_with_cors_and_apikey.json b/tests/translator/output/aws-cn/api_with_cors_and_apikey.json new file mode 100644 index 000000000..6050364a1 --- /dev/null +++ b/tests/translator/output/aws-cn/api_with_cors_and_apikey.json @@ -0,0 +1,305 @@ +{ + "AWSTemplateFormatVersion": "2010-09-09", + "Metadata": { + "SamTransformTest": true + }, + "Outputs": { + "ApiUrl": { + "Description": "URL of your API endpoint", + "Value": { + "Fn::Sub": "https://${MyApi}.execute-api.${AWS::Region}.amazonaws.com/dev/" + } + } + }, + "Resources": { + "ApiGatewayLambdaRole": { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "apigateway.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": "lambda:InvokeFunction", + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "AllowInvokeLambdaFunctions" + } + ] + }, + "Type": "AWS::IAM::Role" + }, + "MyApi": { + "Properties": { + "Body": { + "components": { + "securitySchemes": { + "api_key": { + "in": "header", + "name": "x-api-key", + "type": "apiKey" + } + } + }, + "openapi": "3.0.1", + "paths": { + "/apione": { + "get": { + "security": [ + { + "api_key": [] + } + ], + "x-amazon-apigateway-integration": { + "credentials": { + "Fn::Sub": "${ApiGatewayLambdaRole.Arn}" + }, + "httpMethod": "POST", + "passthroughBehavior": "when_no_match", + "type": "aws_proxy", + "uri": { + "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${MyFunction.Arn}/invocations" + } + } + }, + "options": { + "responses": { + "200": { + "description": "Default response for CORS method", + "headers": { + "Access-Control-Allow-Headers": { + "schema": { + "schema": { + "type": "string" + } + } + }, + "Access-Control-Allow-Methods": { + "schema": { + "schema": { + "type": "string" + } + } + }, + "Access-Control-Allow-Origin": { + "schema": { + "schema": { + "type": "string" + } + } + }, + "Access-Control-Max-Age": { + "schema": { + "schema": { + "type": "integer" + } + } + } + } + } + }, + "summary": "CORS support", + "x-amazon-apigateway-integration": { + "requestTemplates": { + "application/json": "{\n \"statusCode\" : 200\n}\n" + }, + "responses": { + "default": { + "responseParameters": { + "method.response.header.Access-Control-Allow-Headers": "'headers'", + "method.response.header.Access-Control-Allow-Methods": "'methods'", + "method.response.header.Access-Control-Allow-Origin": "'origins'", + "method.response.header.Access-Control-Max-Age": "'600'" + }, + "responseTemplates": { + "application/json": "{}\n" + }, + "statusCode": "200" + } + }, + "type": "mock" + } + } + }, + "/apitwo": { + "get": { + "security": [ + { + "api_key": [] + } + ], + "x-amazon-apigateway-integration": { + "credentials": { + "Fn::Sub": "${ApiGatewayLambdaRole.Arn}" + }, + "httpMethod": "POST", + "passthroughBehavior": "when_no_match", + "type": "aws_proxy", + "uri": { + "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${MyFunction.Arn}/invocations" + } + } + }, + "options": { + "responses": { + "200": { + "description": "Default response for CORS method", + "headers": { + "Access-Control-Allow-Headers": { + "schema": { + "schema": { + "type": "string" + } + } + }, + "Access-Control-Allow-Methods": { + "schema": { + "schema": { + "type": "string" + } + } + }, + "Access-Control-Allow-Origin": { + "schema": { + "schema": { + "type": "string" + } + } + }, + "Access-Control-Max-Age": { + "schema": { + "schema": { + "type": "integer" + } + } + } + } + } + }, + "summary": "CORS support", + "x-amazon-apigateway-integration": { + "requestTemplates": { + "application/json": "{\n \"statusCode\" : 200\n}\n" + }, + "responses": { + "default": { + "responseParameters": { + "method.response.header.Access-Control-Allow-Headers": "'headers'", + "method.response.header.Access-Control-Allow-Methods": "'methods'", + "method.response.header.Access-Control-Allow-Origin": "'origins'", + "method.response.header.Access-Control-Max-Age": "'600'" + }, + "responseTemplates": { + "application/json": "{}\n" + }, + "statusCode": "200" + } + }, + "type": "mock" + } + } + } + } + }, + "EndpointConfiguration": { + "Types": [ + "REGIONAL" + ] + }, + "Parameters": { + "endpointConfigurationTypes": "REGIONAL" + } + }, + "Type": "AWS::ApiGateway::RestApi" + }, + "MyApiDeployment996e54a322": { + "Properties": { + "Description": "RestApi deployment id: 996e54a3224f4396bddbafc7ca104cb3685eccdd", + "RestApiId": { + "Ref": "MyApi" + }, + "StageName": "Stage" + }, + "Type": "AWS::ApiGateway::Deployment" + }, + "MyApidevStage": { + "Properties": { + "DeploymentId": { + "Ref": "MyApiDeployment996e54a322" + }, + "RestApiId": { + "Ref": "MyApi" + }, + "StageName": "dev" + }, + "Type": "AWS::ApiGateway::Stage" + }, + "MyFunction": { + "Properties": { + "Code": { + "ZipFile": "exports.handler = async function (event) {\n return {\n statusCode: 200,\n body: JSON.stringify({ message: \"Hello, SAM!\" }),\n }\n}\n" + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "MyFunctionRole", + "Arn" + ] + }, + "Runtime": "nodejs16.x", + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + }, + "Type": "AWS::Lambda::Function" + }, + "MyFunctionRole": { + "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" + } + } +} diff --git a/tests/translator/output/aws-us-gov/api_with_cors_and_apikey.json b/tests/translator/output/aws-us-gov/api_with_cors_and_apikey.json new file mode 100644 index 000000000..78e299269 --- /dev/null +++ b/tests/translator/output/aws-us-gov/api_with_cors_and_apikey.json @@ -0,0 +1,305 @@ +{ + "AWSTemplateFormatVersion": "2010-09-09", + "Metadata": { + "SamTransformTest": true + }, + "Outputs": { + "ApiUrl": { + "Description": "URL of your API endpoint", + "Value": { + "Fn::Sub": "https://${MyApi}.execute-api.${AWS::Region}.amazonaws.com/dev/" + } + } + }, + "Resources": { + "ApiGatewayLambdaRole": { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "apigateway.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": "lambda:InvokeFunction", + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "AllowInvokeLambdaFunctions" + } + ] + }, + "Type": "AWS::IAM::Role" + }, + "MyApi": { + "Properties": { + "Body": { + "components": { + "securitySchemes": { + "api_key": { + "in": "header", + "name": "x-api-key", + "type": "apiKey" + } + } + }, + "openapi": "3.0.1", + "paths": { + "/apione": { + "get": { + "security": [ + { + "api_key": [] + } + ], + "x-amazon-apigateway-integration": { + "credentials": { + "Fn::Sub": "${ApiGatewayLambdaRole.Arn}" + }, + "httpMethod": "POST", + "passthroughBehavior": "when_no_match", + "type": "aws_proxy", + "uri": { + "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${MyFunction.Arn}/invocations" + } + } + }, + "options": { + "responses": { + "200": { + "description": "Default response for CORS method", + "headers": { + "Access-Control-Allow-Headers": { + "schema": { + "schema": { + "type": "string" + } + } + }, + "Access-Control-Allow-Methods": { + "schema": { + "schema": { + "type": "string" + } + } + }, + "Access-Control-Allow-Origin": { + "schema": { + "schema": { + "type": "string" + } + } + }, + "Access-Control-Max-Age": { + "schema": { + "schema": { + "type": "integer" + } + } + } + } + } + }, + "summary": "CORS support", + "x-amazon-apigateway-integration": { + "requestTemplates": { + "application/json": "{\n \"statusCode\" : 200\n}\n" + }, + "responses": { + "default": { + "responseParameters": { + "method.response.header.Access-Control-Allow-Headers": "'headers'", + "method.response.header.Access-Control-Allow-Methods": "'methods'", + "method.response.header.Access-Control-Allow-Origin": "'origins'", + "method.response.header.Access-Control-Max-Age": "'600'" + }, + "responseTemplates": { + "application/json": "{}\n" + }, + "statusCode": "200" + } + }, + "type": "mock" + } + } + }, + "/apitwo": { + "get": { + "security": [ + { + "api_key": [] + } + ], + "x-amazon-apigateway-integration": { + "credentials": { + "Fn::Sub": "${ApiGatewayLambdaRole.Arn}" + }, + "httpMethod": "POST", + "passthroughBehavior": "when_no_match", + "type": "aws_proxy", + "uri": { + "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${MyFunction.Arn}/invocations" + } + } + }, + "options": { + "responses": { + "200": { + "description": "Default response for CORS method", + "headers": { + "Access-Control-Allow-Headers": { + "schema": { + "schema": { + "type": "string" + } + } + }, + "Access-Control-Allow-Methods": { + "schema": { + "schema": { + "type": "string" + } + } + }, + "Access-Control-Allow-Origin": { + "schema": { + "schema": { + "type": "string" + } + } + }, + "Access-Control-Max-Age": { + "schema": { + "schema": { + "type": "integer" + } + } + } + } + } + }, + "summary": "CORS support", + "x-amazon-apigateway-integration": { + "requestTemplates": { + "application/json": "{\n \"statusCode\" : 200\n}\n" + }, + "responses": { + "default": { + "responseParameters": { + "method.response.header.Access-Control-Allow-Headers": "'headers'", + "method.response.header.Access-Control-Allow-Methods": "'methods'", + "method.response.header.Access-Control-Allow-Origin": "'origins'", + "method.response.header.Access-Control-Max-Age": "'600'" + }, + "responseTemplates": { + "application/json": "{}\n" + }, + "statusCode": "200" + } + }, + "type": "mock" + } + } + } + } + }, + "EndpointConfiguration": { + "Types": [ + "REGIONAL" + ] + }, + "Parameters": { + "endpointConfigurationTypes": "REGIONAL" + } + }, + "Type": "AWS::ApiGateway::RestApi" + }, + "MyApiDeployment996e54a322": { + "Properties": { + "Description": "RestApi deployment id: 996e54a3224f4396bddbafc7ca104cb3685eccdd", + "RestApiId": { + "Ref": "MyApi" + }, + "StageName": "Stage" + }, + "Type": "AWS::ApiGateway::Deployment" + }, + "MyApidevStage": { + "Properties": { + "DeploymentId": { + "Ref": "MyApiDeployment996e54a322" + }, + "RestApiId": { + "Ref": "MyApi" + }, + "StageName": "dev" + }, + "Type": "AWS::ApiGateway::Stage" + }, + "MyFunction": { + "Properties": { + "Code": { + "ZipFile": "exports.handler = async function (event) {\n return {\n statusCode: 200,\n body: JSON.stringify({ message: \"Hello, SAM!\" }),\n }\n}\n" + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "MyFunctionRole", + "Arn" + ] + }, + "Runtime": "nodejs16.x", + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + }, + "Type": "AWS::Lambda::Function" + }, + "MyFunctionRole": { + "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" + } + } +} From 33a7b90cbb889e8d68c7b463b9747061222b10a9 Mon Sep 17 00:00:00 2001 From: connorrobertson Date: Wed, 1 Mar 2023 17:43:03 -0800 Subject: [PATCH 04/10] Ran make pr again --- samtranslator/model/api/api_generator.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/samtranslator/model/api/api_generator.py b/samtranslator/model/api/api_generator.py index bc66b0c16..ff8fcc70e 100644 --- a/samtranslator/model/api/api_generator.py +++ b/samtranslator/model/api/api_generator.py @@ -1218,7 +1218,9 @@ def _set_default_authorizer( add_default_auth_to_preflight=add_default_auth_to_preflight, ) - def _set_default_apikey_required(self, swagger_editor: SwaggerEditor, AddApiKeyRequiredToCorsPreflight: bool) -> None: + def _set_default_apikey_required( + self, swagger_editor: SwaggerEditor, AddApiKeyRequiredToCorsPreflight: bool + ) -> None: for path in swagger_editor.iter_on_path(): swagger_editor.set_path_default_apikey_required(path, AddApiKeyRequiredToCorsPreflight) From 68c2e2cd87562eb5ecbbe17d2c86d05f9c0758ab Mon Sep 17 00:00:00 2001 From: connorrobertson Date: Wed, 1 Mar 2023 17:55:12 -0800 Subject: [PATCH 05/10] Put auto-deploy back --- samtranslator/model/api/api_generator.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/samtranslator/model/api/api_generator.py b/samtranslator/model/api/api_generator.py index ff8fcc70e..d2187e2d4 100644 --- a/samtranslator/model/api/api_generator.py +++ b/samtranslator/model/api/api_generator.py @@ -195,6 +195,7 @@ def __init__( # noqa: too-many-arguments description: Optional[Intrinsicable[str]] = None, mode: Optional[Intrinsicable[str]] = None, api_key_source_type: Optional[Intrinsicable[str]] = None, + always_deploy: Optional[bool] = False, ): """Constructs an API Generator class that generates API Gateway resources @@ -250,6 +251,7 @@ def __init__( # noqa: too-many-arguments self.template_conditions = template_conditions self.mode = mode self.api_key_source_type = api_key_source_type + self.always_deploy = always_deploy def _construct_rest_api(self) -> ApiGatewayRestApi: """Constructs and returns the ApiGateway RestApi. @@ -426,7 +428,12 @@ def _construct_stage( if swagger is not None: deployment.make_auto_deployable( - stage, self.remove_extra_stage, swagger, self.domain, redeploy_restapi_parameters + stage, + self.remove_extra_stage, + swagger, + self.domain, + redeploy_restapi_parameters, + self.always_deploy, ) if self.tags is not None: From c5fe0cfac1c00ca56d91b0badab0ae29a9c39ee4 Mon Sep 17 00:00:00 2001 From: connorrobertson Date: Thu, 2 Mar 2023 10:57:03 -0800 Subject: [PATCH 06/10] Add second transform test + fixed logic --- integration/combination/test_api_with_cors.py | 6 +- samtranslator/model/api/api_generator.py | 6 +- samtranslator/swagger/swagger.py | 8 +- ..._cors_and_apikey_defined_at_api_level.yaml | 66 ++++++ ..._cors_and_apikey_defined_at_api_level.json | 200 +++++++++++++++++ ..._cors_and_apikey_defined_at_api_level.json | 208 ++++++++++++++++++ ..._cors_and_apikey_defined_at_api_level.json | 208 ++++++++++++++++++ 7 files changed, 693 insertions(+), 9 deletions(-) create mode 100644 tests/translator/input/api_with_cors_and_apikey_defined_at_api_level.yaml create mode 100644 tests/translator/output/api_with_cors_and_apikey_defined_at_api_level.json create mode 100644 tests/translator/output/aws-cn/api_with_cors_and_apikey_defined_at_api_level.json create mode 100644 tests/translator/output/aws-us-gov/api_with_cors_and_apikey_defined_at_api_level.json diff --git a/integration/combination/test_api_with_cors.py b/integration/combination/test_api_with_cors.py index 5906e9d89..e6babcc8f 100644 --- a/integration/combination/test_api_with_cors.py +++ b/integration/combination/test_api_with_cors.py @@ -12,7 +12,11 @@ @skipIf(current_region_does_not_support([REST_API]), "Rest API is not supported in this testing region") class TestApiWithCors(BaseTest): @parameterized.expand( - ["combination/api_with_cors", "combination/api_with_cors_openapi", "combination/api_with_cors_and_apikey"] + [ + "combination/api_with_cors", + "combination/api_with_cors_openapi", + "combination/api_with_cors_and_apikey", + ] ) def test_cors(self, file_name): self.create_and_verify_stack(file_name) diff --git a/samtranslator/model/api/api_generator.py b/samtranslator/model/api/api_generator.py index d2187e2d4..bb03a827f 100644 --- a/samtranslator/model/api/api_generator.py +++ b/samtranslator/model/api/api_generator.py @@ -1225,11 +1225,9 @@ def _set_default_authorizer( add_default_auth_to_preflight=add_default_auth_to_preflight, ) - def _set_default_apikey_required( - self, swagger_editor: SwaggerEditor, AddApiKeyRequiredToCorsPreflight: bool - ) -> None: + def _set_default_apikey_required(self, swagger_editor: SwaggerEditor, required_options_api_key: bool) -> None: for path in swagger_editor.iter_on_path(): - swagger_editor.set_path_default_apikey_required(path, AddApiKeyRequiredToCorsPreflight) + swagger_editor.set_path_default_apikey_required(path, required_options_api_key) def _set_endpoint_configuration(self, rest_api: ApiGatewayRestApi, value: Union[str, Dict[str, Any]]) -> None: """ diff --git a/samtranslator/swagger/swagger.py b/samtranslator/swagger/swagger.py index 39cd1ad85..0d7927026 100644 --- a/samtranslator/swagger/swagger.py +++ b/samtranslator/swagger/swagger.py @@ -612,7 +612,7 @@ def set_path_default_authorizer( # noqa: too-many-branches if "AWS_IAM" in method_definition["security"][0]: self.add_awsiam_security_definition() - def set_path_default_apikey_required(self, path: str, AddApiKeyRequiredToCorsPreflight: bool = True) -> None: + def set_path_default_apikey_required(self, path: str, required_options_api_key: bool = True) -> None: """ Add the ApiKey security as required for each method on this path unless ApiKeyRequired was defined at the Function/Path/Method level. This is intended to be used to set the @@ -620,7 +620,7 @@ def set_path_default_apikey_required(self, path: str, AddApiKeyRequiredToCorsPre Serverless API. :param string path: Path name - :param bool AddApiKeyRequiredToCorsPreflight: Bool of whether to add the ApiKeyRequired + :param bool required_options_api_key: Bool of whether to add the ApiKeyRequired to OPTIONS preflight requests. """ @@ -675,7 +675,7 @@ def set_path_default_apikey_required(self, path: str, AddApiKeyRequiredToCorsPre security = existing_non_apikey_security + apikey_security - if method_name == "options" and not AddApiKeyRequiredToCorsPreflight: + if method_name == "options" and not required_options_api_key: security = existing_non_apikey_security if security != existing_security: @@ -701,7 +701,7 @@ def add_auth_to_method(self, path: str, method_name: str, auth: Dict[str, Any], method_apikey_required = auth and auth.get("ApiKeyRequired") - if auth.get("AddApiKeyRequiredToCorsPreflight") and method_name == "options": + if not auth.get("AddApiKeyRequiredToCorsPreflight") and method_name == "options": method_apikey_required = False if method_apikey_required is not None: diff --git a/tests/translator/input/api_with_cors_and_apikey_defined_at_api_level.yaml b/tests/translator/input/api_with_cors_and_apikey_defined_at_api_level.yaml new file mode 100644 index 000000000..dcabe6bfb --- /dev/null +++ b/tests/translator/input/api_with_cors_and_apikey_defined_at_api_level.yaml @@ -0,0 +1,66 @@ +AWSTemplateFormatVersion: '2010-09-09' + +Transform: +- AWS::Serverless-2016-10-31 + +Resources: + + MyFunction: + Type: AWS::Serverless::Function + Properties: + Handler: index.handler + InlineCode: | + exports.handler = async function (event) { + return { + statusCode: 200, + body: JSON.stringify({ message: "Hello, SAM!" }), + } + } + Runtime: nodejs16.x + + ApiGatewayLambdaRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Principal: {Service: apigateway.amazonaws.com} + Action: sts:AssumeRole + Policies: + - PolicyName: AllowInvokeLambdaFunctions + PolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Action: lambda:InvokeFunction + Resource: '*' + + MyApi: + Type: AWS::Serverless::Api + Properties: + Cors: "'*'" + Auth: + ApiKeyRequired: true + AddApiKeyRequiredToCorsPreflight: false + StageName: dev + DefinitionBody: + openapi: 3.0.1 + paths: + /hello: + get: + x-amazon-apigateway-integration: + credentials: + Fn::Sub: ${ApiGatewayLambdaRole.Arn} + uri: + Fn::Sub: arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${MyFunction.Arn}/invocations + passthroughBehavior: when_no_match + httpMethod: POST + type: aws_proxy + + + +Outputs: + WebEndpoint: + Description: API Gateway endpoint URL + Value: !Sub "https://${MyApi}.execute-api.${AWS::Region}.amazonaws.com/dev/hello" diff --git a/tests/translator/output/api_with_cors_and_apikey_defined_at_api_level.json b/tests/translator/output/api_with_cors_and_apikey_defined_at_api_level.json new file mode 100644 index 000000000..eca1c0bdb --- /dev/null +++ b/tests/translator/output/api_with_cors_and_apikey_defined_at_api_level.json @@ -0,0 +1,200 @@ +{ + "AWSTemplateFormatVersion": "2010-09-09", + "Outputs": { + "WebEndpoint": { + "Description": "API Gateway endpoint URL", + "Value": { + "Fn::Sub": "https://${MyApi}.execute-api.${AWS::Region}.amazonaws.com/dev/hello" + } + } + }, + "Resources": { + "ApiGatewayLambdaRole": { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "apigateway.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": "lambda:InvokeFunction", + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "AllowInvokeLambdaFunctions" + } + ] + }, + "Type": "AWS::IAM::Role" + }, + "MyApi": { + "Properties": { + "Body": { + "components": { + "securitySchemes": { + "api_key": { + "in": "header", + "name": "x-api-key", + "type": "apiKey" + } + } + }, + "openapi": "3.0.1", + "paths": { + "/hello": { + "get": { + "security": [ + { + "api_key": [] + } + ], + "x-amazon-apigateway-integration": { + "credentials": { + "Fn::Sub": "${ApiGatewayLambdaRole.Arn}" + }, + "httpMethod": "POST", + "passthroughBehavior": "when_no_match", + "type": "aws_proxy", + "uri": { + "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${MyFunction.Arn}/invocations" + } + } + }, + "options": { + "responses": { + "200": { + "description": "Default response for CORS method", + "headers": { + "Access-Control-Allow-Methods": { + "schema": { + "schema": { + "type": "string" + } + } + }, + "Access-Control-Allow-Origin": { + "schema": { + "schema": { + "type": "string" + } + } + } + } + } + }, + "summary": "CORS support", + "x-amazon-apigateway-integration": { + "requestTemplates": { + "application/json": "{\n \"statusCode\" : 200\n}\n" + }, + "responses": { + "default": { + "responseParameters": { + "method.response.header.Access-Control-Allow-Methods": "'GET,OPTIONS'", + "method.response.header.Access-Control-Allow-Origin": "'*'" + }, + "responseTemplates": { + "application/json": "{}\n" + }, + "statusCode": "200" + } + }, + "type": "mock" + } + } + } + } + } + }, + "Type": "AWS::ApiGateway::RestApi" + }, + "MyApiDeploymentdb7bc48ce9": { + "Properties": { + "Description": "RestApi deployment id: db7bc48ce9273479c81f6c8c0b91862e71dbbef8", + "RestApiId": { + "Ref": "MyApi" + }, + "StageName": "Stage" + }, + "Type": "AWS::ApiGateway::Deployment" + }, + "MyApidevStage": { + "Properties": { + "DeploymentId": { + "Ref": "MyApiDeploymentdb7bc48ce9" + }, + "RestApiId": { + "Ref": "MyApi" + }, + "StageName": "dev" + }, + "Type": "AWS::ApiGateway::Stage" + }, + "MyFunction": { + "Properties": { + "Code": { + "ZipFile": "exports.handler = async function (event) {\n return {\n statusCode: 200,\n body: JSON.stringify({ message: \"Hello, SAM!\" }),\n }\n}\n" + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "MyFunctionRole", + "Arn" + ] + }, + "Runtime": "nodejs16.x", + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + }, + "Type": "AWS::Lambda::Function" + }, + "MyFunctionRole": { + "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" + } + } +} diff --git a/tests/translator/output/aws-cn/api_with_cors_and_apikey_defined_at_api_level.json b/tests/translator/output/aws-cn/api_with_cors_and_apikey_defined_at_api_level.json new file mode 100644 index 000000000..372a3d807 --- /dev/null +++ b/tests/translator/output/aws-cn/api_with_cors_and_apikey_defined_at_api_level.json @@ -0,0 +1,208 @@ +{ + "AWSTemplateFormatVersion": "2010-09-09", + "Outputs": { + "WebEndpoint": { + "Description": "API Gateway endpoint URL", + "Value": { + "Fn::Sub": "https://${MyApi}.execute-api.${AWS::Region}.amazonaws.com/dev/hello" + } + } + }, + "Resources": { + "ApiGatewayLambdaRole": { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "apigateway.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": "lambda:InvokeFunction", + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "AllowInvokeLambdaFunctions" + } + ] + }, + "Type": "AWS::IAM::Role" + }, + "MyApi": { + "Properties": { + "Body": { + "components": { + "securitySchemes": { + "api_key": { + "in": "header", + "name": "x-api-key", + "type": "apiKey" + } + } + }, + "openapi": "3.0.1", + "paths": { + "/hello": { + "get": { + "security": [ + { + "api_key": [] + } + ], + "x-amazon-apigateway-integration": { + "credentials": { + "Fn::Sub": "${ApiGatewayLambdaRole.Arn}" + }, + "httpMethod": "POST", + "passthroughBehavior": "when_no_match", + "type": "aws_proxy", + "uri": { + "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${MyFunction.Arn}/invocations" + } + } + }, + "options": { + "responses": { + "200": { + "description": "Default response for CORS method", + "headers": { + "Access-Control-Allow-Methods": { + "schema": { + "schema": { + "type": "string" + } + } + }, + "Access-Control-Allow-Origin": { + "schema": { + "schema": { + "type": "string" + } + } + } + } + } + }, + "summary": "CORS support", + "x-amazon-apigateway-integration": { + "requestTemplates": { + "application/json": "{\n \"statusCode\" : 200\n}\n" + }, + "responses": { + "default": { + "responseParameters": { + "method.response.header.Access-Control-Allow-Methods": "'GET,OPTIONS'", + "method.response.header.Access-Control-Allow-Origin": "'*'" + }, + "responseTemplates": { + "application/json": "{}\n" + }, + "statusCode": "200" + } + }, + "type": "mock" + } + } + } + } + }, + "EndpointConfiguration": { + "Types": [ + "REGIONAL" + ] + }, + "Parameters": { + "endpointConfigurationTypes": "REGIONAL" + } + }, + "Type": "AWS::ApiGateway::RestApi" + }, + "MyApiDeploymentdb7bc48ce9": { + "Properties": { + "Description": "RestApi deployment id: db7bc48ce9273479c81f6c8c0b91862e71dbbef8", + "RestApiId": { + "Ref": "MyApi" + }, + "StageName": "Stage" + }, + "Type": "AWS::ApiGateway::Deployment" + }, + "MyApidevStage": { + "Properties": { + "DeploymentId": { + "Ref": "MyApiDeploymentdb7bc48ce9" + }, + "RestApiId": { + "Ref": "MyApi" + }, + "StageName": "dev" + }, + "Type": "AWS::ApiGateway::Stage" + }, + "MyFunction": { + "Properties": { + "Code": { + "ZipFile": "exports.handler = async function (event) {\n return {\n statusCode: 200,\n body: JSON.stringify({ message: \"Hello, SAM!\" }),\n }\n}\n" + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "MyFunctionRole", + "Arn" + ] + }, + "Runtime": "nodejs16.x", + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + }, + "Type": "AWS::Lambda::Function" + }, + "MyFunctionRole": { + "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" + } + } +} diff --git a/tests/translator/output/aws-us-gov/api_with_cors_and_apikey_defined_at_api_level.json b/tests/translator/output/aws-us-gov/api_with_cors_and_apikey_defined_at_api_level.json new file mode 100644 index 000000000..f6fe0d9ec --- /dev/null +++ b/tests/translator/output/aws-us-gov/api_with_cors_and_apikey_defined_at_api_level.json @@ -0,0 +1,208 @@ +{ + "AWSTemplateFormatVersion": "2010-09-09", + "Outputs": { + "WebEndpoint": { + "Description": "API Gateway endpoint URL", + "Value": { + "Fn::Sub": "https://${MyApi}.execute-api.${AWS::Region}.amazonaws.com/dev/hello" + } + } + }, + "Resources": { + "ApiGatewayLambdaRole": { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "apigateway.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": "lambda:InvokeFunction", + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "AllowInvokeLambdaFunctions" + } + ] + }, + "Type": "AWS::IAM::Role" + }, + "MyApi": { + "Properties": { + "Body": { + "components": { + "securitySchemes": { + "api_key": { + "in": "header", + "name": "x-api-key", + "type": "apiKey" + } + } + }, + "openapi": "3.0.1", + "paths": { + "/hello": { + "get": { + "security": [ + { + "api_key": [] + } + ], + "x-amazon-apigateway-integration": { + "credentials": { + "Fn::Sub": "${ApiGatewayLambdaRole.Arn}" + }, + "httpMethod": "POST", + "passthroughBehavior": "when_no_match", + "type": "aws_proxy", + "uri": { + "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${MyFunction.Arn}/invocations" + } + } + }, + "options": { + "responses": { + "200": { + "description": "Default response for CORS method", + "headers": { + "Access-Control-Allow-Methods": { + "schema": { + "schema": { + "type": "string" + } + } + }, + "Access-Control-Allow-Origin": { + "schema": { + "schema": { + "type": "string" + } + } + } + } + } + }, + "summary": "CORS support", + "x-amazon-apigateway-integration": { + "requestTemplates": { + "application/json": "{\n \"statusCode\" : 200\n}\n" + }, + "responses": { + "default": { + "responseParameters": { + "method.response.header.Access-Control-Allow-Methods": "'GET,OPTIONS'", + "method.response.header.Access-Control-Allow-Origin": "'*'" + }, + "responseTemplates": { + "application/json": "{}\n" + }, + "statusCode": "200" + } + }, + "type": "mock" + } + } + } + } + }, + "EndpointConfiguration": { + "Types": [ + "REGIONAL" + ] + }, + "Parameters": { + "endpointConfigurationTypes": "REGIONAL" + } + }, + "Type": "AWS::ApiGateway::RestApi" + }, + "MyApiDeploymentdb7bc48ce9": { + "Properties": { + "Description": "RestApi deployment id: db7bc48ce9273479c81f6c8c0b91862e71dbbef8", + "RestApiId": { + "Ref": "MyApi" + }, + "StageName": "Stage" + }, + "Type": "AWS::ApiGateway::Deployment" + }, + "MyApidevStage": { + "Properties": { + "DeploymentId": { + "Ref": "MyApiDeploymentdb7bc48ce9" + }, + "RestApiId": { + "Ref": "MyApi" + }, + "StageName": "dev" + }, + "Type": "AWS::ApiGateway::Stage" + }, + "MyFunction": { + "Properties": { + "Code": { + "ZipFile": "exports.handler = async function (event) {\n return {\n statusCode: 200,\n body: JSON.stringify({ message: \"Hello, SAM!\" }),\n }\n}\n" + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "MyFunctionRole", + "Arn" + ] + }, + "Runtime": "nodejs16.x", + "Tags": [ + { + "Key": "lambda:createdBy", + "Value": "SAM" + } + ] + }, + "Type": "AWS::Lambda::Function" + }, + "MyFunctionRole": { + "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" + } + } +} From 918f24f85c949bcfc68ae9b96bb246a81e645cf2 Mon Sep 17 00:00:00 2001 From: Connor Robertson Date: Thu, 2 Mar 2023 12:54:56 -0800 Subject: [PATCH 07/10] Update samtranslator/swagger/swagger.py Easier to read Co-authored-by: Christoffer Rehn <1280602+hoffa@users.noreply.github.com> --- samtranslator/swagger/swagger.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samtranslator/swagger/swagger.py b/samtranslator/swagger/swagger.py index 0d7927026..9c56b6566 100644 --- a/samtranslator/swagger/swagger.py +++ b/samtranslator/swagger/swagger.py @@ -701,7 +701,7 @@ def add_auth_to_method(self, path: str, method_name: str, auth: Dict[str, Any], method_apikey_required = auth and auth.get("ApiKeyRequired") - if not auth.get("AddApiKeyRequiredToCorsPreflight") and method_name == "options": + if method_name == "options" and not auth.get("AddApiKeyRequiredToCorsPreflight"): method_apikey_required = False if method_apikey_required is not None: From 34083f437ee885c0beb2c0b3f0b02518295b64f9 Mon Sep 17 00:00:00 2001 From: connorrobertson Date: Fri, 3 Mar 2023 12:54:08 -0800 Subject: [PATCH 08/10] Requested fixes --- samtranslator/swagger/swagger.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/samtranslator/swagger/swagger.py b/samtranslator/swagger/swagger.py index 9c56b6566..12731364a 100644 --- a/samtranslator/swagger/swagger.py +++ b/samtranslator/swagger/swagger.py @@ -696,12 +696,13 @@ def add_auth_to_method(self, path: str, method_name: str, auth: Dict[str, Any], method_scopes = auth and auth.get("AuthorizationScopes") api_auth = api and api.get("Auth") authorizers = api_auth and api_auth.get("Authorizers") + required_options_api_key = method_name == "options" and (auth.get("AddApiKeyRequiredToCorsPreflight") is False) if method_authorizer: self._set_method_authorizer(path, method_name, method_authorizer, authorizers, method_scopes) # type: ignore[no-untyped-call] method_apikey_required = auth and auth.get("ApiKeyRequired") - if method_name == "options" and not auth.get("AddApiKeyRequiredToCorsPreflight"): + if required_options_api_key: method_apikey_required = False if method_apikey_required is not None: From 65ada4762a71fdd3f7b913d24e5df76141d80579 Mon Sep 17 00:00:00 2001 From: connorrobertson Date: Mon, 6 Mar 2023 16:55:33 -0800 Subject: [PATCH 09/10] Requested changes --- samtranslator/swagger/swagger.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/samtranslator/swagger/swagger.py b/samtranslator/swagger/swagger.py index 12731364a..505a72635 100644 --- a/samtranslator/swagger/swagger.py +++ b/samtranslator/swagger/swagger.py @@ -1,4 +1,4 @@ -import copy +import copy import re from typing import Any, Callable, Dict, Optional, TypeVar @@ -11,6 +11,7 @@ from samtranslator.translator.arn_generator import ArnGenerator from samtranslator.utils.py27hash_fix import Py27Dict, Py27UniStr from samtranslator.utils.utils import InvalidValueType, dict_deep_set +from samtranslator.validator.value_validator import sam_expect T = TypeVar("T") @@ -696,15 +697,12 @@ def add_auth_to_method(self, path: str, method_name: str, auth: Dict[str, Any], method_scopes = auth and auth.get("AuthorizationScopes") api_auth = api and api.get("Auth") authorizers = api_auth and api_auth.get("Authorizers") - required_options_api_key = method_name == "options" and (auth.get("AddApiKeyRequiredToCorsPreflight") is False) + if method_authorizer: self._set_method_authorizer(path, method_name, method_authorizer, authorizers, method_scopes) # type: ignore[no-untyped-call] method_apikey_required = auth and auth.get("ApiKeyRequired") - if required_options_api_key: - method_apikey_required = False - if method_apikey_required is not None: self._set_method_apikey_handling(path, method_name, method_apikey_required) # type: ignore[no-untyped-call] From 7a0e18c1f2c6e8610c8e711893068b110a6766d4 Mon Sep 17 00:00:00 2001 From: connorrobertson Date: Mon, 6 Mar 2023 17:01:08 -0800 Subject: [PATCH 10/10] Make format --- samtranslator/swagger/swagger.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/samtranslator/swagger/swagger.py b/samtranslator/swagger/swagger.py index 505a72635..96e57d53c 100644 --- a/samtranslator/swagger/swagger.py +++ b/samtranslator/swagger/swagger.py @@ -1,4 +1,4 @@ -import copy +import copy import re from typing import Any, Callable, Dict, Optional, TypeVar @@ -11,7 +11,6 @@ from samtranslator.translator.arn_generator import ArnGenerator from samtranslator.utils.py27hash_fix import Py27Dict, Py27UniStr from samtranslator.utils.utils import InvalidValueType, dict_deep_set -from samtranslator.validator.value_validator import sam_expect T = TypeVar("T") @@ -697,7 +696,7 @@ def add_auth_to_method(self, path: str, method_name: str, auth: Dict[str, Any], method_scopes = auth and auth.get("AuthorizationScopes") api_auth = api and api.get("Auth") authorizers = api_auth and api_auth.get("Authorizers") - + if method_authorizer: self._set_method_authorizer(path, method_name, method_authorizer, authorizers, method_scopes) # type: ignore[no-untyped-call]