From 7d5827cc31ba1eeaea95a4c441e77e52d20910f3 Mon Sep 17 00:00:00 2001 From: Xia Zhao <xazhao@amazon.com> Date: Mon, 4 Mar 2024 15:23:36 -0800 Subject: [PATCH 1/5] fix: move normalize the open api version into feature toggle --- samtranslator/model/api/api_generator.py | 10 +- samtranslator/model/sam_resources.py | 2 + .../input/feature_toggle_config.json | 17 ++ ..._toggle_api_open_api_version_override.yaml | 166 +++++++++++ .../output/api_open_api_version_override.json | 35 ++- .../aws-cn/api_open_api_version_override.json | 35 ++- ..._toggle_api_open_api_version_override.json | 273 ++++++++++++++++++ .../api_open_api_version_override.json | 35 ++- ..._toggle_api_open_api_version_override.json | 273 ++++++++++++++++++ ..._toggle_api_open_api_version_override.json | 265 +++++++++++++++++ tests/translator/test_translator.py | 39 ++- 11 files changed, 1107 insertions(+), 43 deletions(-) create mode 100644 tests/translator/input/feature_toggle_api_open_api_version_override.yaml create mode 100644 tests/translator/output/aws-cn/feature_toggle_api_open_api_version_override.json create mode 100644 tests/translator/output/aws-us-gov/feature_toggle_api_open_api_version_override.json create mode 100644 tests/translator/output/feature_toggle_api_open_api_version_override.json diff --git a/samtranslator/model/api/api_generator.py b/samtranslator/model/api/api_generator.py index 1e0052ce2..e9b12461a 100644 --- a/samtranslator/model/api/api_generator.py +++ b/samtranslator/model/api/api_generator.py @@ -3,6 +3,7 @@ from dataclasses import dataclass from typing import Any, Dict, List, Optional, Set, Tuple, Union, cast +from samtranslator.feature_toggle.feature_toggle import FeatureToggle from samtranslator.metrics.method_decorator import cw_timer from samtranslator.model import Resource from samtranslator.model.apigateway import ( @@ -205,6 +206,7 @@ def __init__( # noqa: PLR0913 mode: Optional[Intrinsicable[str]] = None, api_key_source_type: Optional[Intrinsicable[str]] = None, always_deploy: Optional[bool] = False, + feature_toggle: Optional[FeatureToggle] = None, ): """Constructs an API Generator class that generates API Gateway resources @@ -260,6 +262,7 @@ def __init__( # noqa: PLR0913 self.mode = mode self.api_key_source_type = api_key_source_type self.always_deploy = always_deploy + self.feature_toggle = feature_toggle def _construct_rest_api(self) -> ApiGatewayRestApi: """Constructs and returns the ApiGateway RestApi. @@ -1124,7 +1127,12 @@ def _openapi_postprocess(self, definition_body: Dict[str, Any]) -> Dict[str, Any if definition_body.get("swagger") is not None: return definition_body - normalized_open_api_version = definition_body.get("openapi", self.open_api_version) + if self.feature_toggle and self.feature_toggle.is_enabled("normalized_open_api_version"): + normalized_open_api_version = definition_body.get("openapi", self.open_api_version) + elif definition_body.get("openapi") is not None and self.open_api_version is None: + normalized_open_api_version = definition_body.get("openapi") + else: + normalized_open_api_version = self.open_api_version if normalized_open_api_version and SwaggerEditor.safe_compare_regex_with_string( SwaggerEditor._OPENAPI_VERSION_3_REGEX, normalized_open_api_version diff --git a/samtranslator/model/sam_resources.py b/samtranslator/model/sam_resources.py index 4aeed875c..1b73b97b6 100644 --- a/samtranslator/model/sam_resources.py +++ b/samtranslator/model/sam_resources.py @@ -1293,6 +1293,7 @@ def to_cloudformation(self, **kwargs) -> List[Resource]: # type: ignore[no-unty shared_api_usage_plan = kwargs.get("shared_api_usage_plan") template_conditions = kwargs.get("conditions") route53_record_set_groups = kwargs.get("route53_record_set_groups", {}) + feature_toggle = kwargs.get("feature_toggle") api_generator = ApiGenerator( self.logical_id, @@ -1329,6 +1330,7 @@ def to_cloudformation(self, **kwargs) -> List[Resource]: # type: ignore[no-unty mode=self.Mode, api_key_source_type=self.ApiKeySourceType, always_deploy=self.AlwaysDeploy, + feature_toggle=feature_toggle, ) generated_resources = api_generator.to_cloudformation(redeploy_restapi_parameters, route53_record_set_groups) diff --git a/tests/feature_toggle/input/feature_toggle_config.json b/tests/feature_toggle/input/feature_toggle_config.json index d2d82ca3d..63f9f461c 100644 --- a/tests/feature_toggle/input/feature_toggle_config.json +++ b/tests/feature_toggle/input/feature_toggle_config.json @@ -46,5 +46,22 @@ "enabled": false } } + }, + "normalized_open_api_version": { + "beta": { + "default": { + "enabled": false + } + }, + "gamma": { + "default": { + "enabled": false + } + }, + "prod": { + "default": { + "enabled": false + } + } } } diff --git a/tests/translator/input/feature_toggle_api_open_api_version_override.yaml b/tests/translator/input/feature_toggle_api_open_api_version_override.yaml new file mode 100644 index 000000000..7b325fb43 --- /dev/null +++ b/tests/translator/input/feature_toggle_api_open_api_version_override.yaml @@ -0,0 +1,166 @@ +Transform: +- AWS::Serverless-2016-10-31 +Resources: + ApiGatewayCognitoExecutionRole4F7CB5C8: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Statement: + - Action: sts:AssumeRole + Effect: Allow + Principal: + Service: apigateway.amazonaws.com + Version: '2012-10-17' + Policies: + - PolicyDocument: + Statement: + - Action: lambda:Invoke* + Effect: Allow + Resource: + Fn::GetAtt: + - LambdaFunction7804BD21 + - Arn + Version: '2012-10-17' + PolicyName: apigInvokeLambda + LambdaFunctionServiceRoleD6E423C9: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Statement: + - Action: sts:AssumeRole + Effect: Allow + Principal: + Service: lambda.amazonaws.com + Version: '2012-10-17' + ManagedPolicyArns: + - Fn::Join: + - '' + - - 'arn:' + - Ref: AWS::Partition + - :iam::aws:policy/service-role/AWSLambdaBasicExecutionRole + LambdaFunctionServiceRoleDefaultPolicyF01A7EDC: + Type: AWS::IAM::Policy + Properties: + PolicyDocument: + Statement: + - Action: sns:Publish + Effect: Allow + Resource: '*' + Version: '2012-10-17' + PolicyName: LambdaFunctionServiceRoleDefaultPolicyF01A7EDC + Roles: + - Ref: LambdaFunctionServiceRoleD6E423C9 + LambdaFunction7804BD21: + Type: AWS::Lambda::Function + Properties: + Code: + ZipFile: | + exports.handler = async (event, context, callback) => { + const auth = event.queryStringParameters.authorization + const policyDocument = { + Version: '2012-10-17', + Statement: [{ + Action: 'execute-api:Invoke', + Effect: auth && auth.toLowerCase() === 'allow' ? 'Allow' : 'Deny', + Resource: event.methodArn + }] + } + + return { + principalId: 'user', + context: {}, + policyDocument + } + } + Role: + Fn::GetAtt: + - LambdaFunctionServiceRoleD6E423C9 + - Arn + Handler: index.handler + Runtime: nodejs16.x + MyCognitoUserPool: + Type: AWS::Cognito::UserPool + Properties: + UserPoolName: MyCognitoUserPool + ApiGatewayCognitoService15108F0B: + Type: AWS::Serverless::Api + Properties: + StageName: prod + Auth: + AddDefaultAuthorizerToCorsPreflight: false + Authorizers: + CognitoAuthorizer: + UserPoolArn: + Fn::GetAtt: MyCognitoUserPool.Arn + DefaultAuthorizer: CognitoAuthorizer + DefinitionBody: + openapi: 3.0.2 + info: + title: RxtHofApprovalServiceLambdaCognito + version: '2018-05-10' + paths: + /reviews: + post: + operationId: CreateReview + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/CreateReviewRequestContent' + required: true + responses: + '200': + description: CreateReview 200 response + headers: + Access-Control-Allow-Origin: + schema: + type: string + Access-Control-Expose-Headers: + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/CreateReviewResponseContent' + x-amazon-apigateway-integration: + type: aws_proxy + httpMethod: POST + uri: + Fn::Sub: arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${LambdaFunction7804BD21.Arn}/invocations + credentials: + Fn::Sub: ${ApiGatewayCognitoExecutionRole4F7CB5C8.Arn} + responses: + default: + statusCode: '200' + responseParameters: + method.response.header.Access-Control-Allow-Origin: "'*'" + method.response.header.Access-Control-Expose-Headers: "'Content-Length,Content-Type,X-Amzn-Errortype,X-Amzn-Requestid'" + components: + schemas: + CreateReviewRequestContent: + type: object + properties: + reviewId: + type: string + CreateReviewResponseContent: + type: object + properties: + reviewId: + type: string + securitySchemes: + aws.auth.sigv4: + type: apiKey + description: AWS Signature Version 4 authentication + name: Authorization + in: header + x-amazon-apigateway-authtype: awsSigv4 + security: + - aws.auth.sigv4: [] + x-amazon-apigateway-gateway-responses: + DEFAULT_5XX: + responseTemplates: + application/json: '{"message":$context.error.messageString}' + responseParameters: + gatewayresponse.header.Access-Control-Allow-Origin: "'*'" + OpenApiVersion: '2.0' + TracingEnabled: true diff --git a/tests/translator/output/api_open_api_version_override.json b/tests/translator/output/api_open_api_version_override.json index 90498cf57..3173c67e2 100644 --- a/tests/translator/output/api_open_api_version_override.json +++ b/tests/translator/output/api_open_api_version_override.json @@ -60,19 +60,12 @@ } }, "securitySchemes": { - "CognitoAuthorizer": { + "aws.auth.sigv4": { + "description": "AWS Signature Version 4 authentication", "in": "header", "name": "Authorization", "type": "apiKey", - "x-amazon-apigateway-authorizer": { - "providerARNs": [ - { - "Fn::GetAtt": "MyCognitoUserPool.Arn" - } - ], - "type": "cognito_user_pools" - }, - "x-amazon-apigateway-authtype": "cognito_user_pools" + "x-amazon-apigateway-authtype": "awsSigv4" } } }, @@ -151,6 +144,22 @@ "aws.auth.sigv4": [] } ], + "securityDefinitions": { + "CognitoAuthorizer": { + "in": "header", + "name": "Authorization", + "type": "apiKey", + "x-amazon-apigateway-authorizer": { + "providerARNs": [ + { + "Fn::GetAtt": "MyCognitoUserPool.Arn" + } + ], + "type": "cognito_user_pools" + }, + "x-amazon-apigateway-authtype": "cognito_user_pools" + } + }, "x-amazon-apigateway-gateway-responses": { "DEFAULT_5XX": { "responseParameters": { @@ -165,9 +174,9 @@ }, "Type": "AWS::ApiGateway::RestApi" }, - "ApiGatewayCognitoService15108F0BDeployment2a9725c838": { + "ApiGatewayCognitoService15108F0BDeployment46d69ebe56": { "Properties": { - "Description": "RestApi deployment id: 2a9725c838d10c88c6c75fec8e5fe7557ff62cea", + "Description": "RestApi deployment id: 46d69ebe56e878706dbfbf454b9cbdf8c15a5343", "RestApiId": { "Ref": "ApiGatewayCognitoService15108F0B" } @@ -177,7 +186,7 @@ "ApiGatewayCognitoService15108F0BprodStage": { "Properties": { "DeploymentId": { - "Ref": "ApiGatewayCognitoService15108F0BDeployment2a9725c838" + "Ref": "ApiGatewayCognitoService15108F0BDeployment46d69ebe56" }, "RestApiId": { "Ref": "ApiGatewayCognitoService15108F0B" diff --git a/tests/translator/output/aws-cn/api_open_api_version_override.json b/tests/translator/output/aws-cn/api_open_api_version_override.json index 2b1ece951..8ab3d795a 100644 --- a/tests/translator/output/aws-cn/api_open_api_version_override.json +++ b/tests/translator/output/aws-cn/api_open_api_version_override.json @@ -60,19 +60,12 @@ } }, "securitySchemes": { - "CognitoAuthorizer": { + "aws.auth.sigv4": { + "description": "AWS Signature Version 4 authentication", "in": "header", "name": "Authorization", "type": "apiKey", - "x-amazon-apigateway-authorizer": { - "providerARNs": [ - { - "Fn::GetAtt": "MyCognitoUserPool.Arn" - } - ], - "type": "cognito_user_pools" - }, - "x-amazon-apigateway-authtype": "cognito_user_pools" + "x-amazon-apigateway-authtype": "awsSigv4" } } }, @@ -151,6 +144,22 @@ "aws.auth.sigv4": [] } ], + "securityDefinitions": { + "CognitoAuthorizer": { + "in": "header", + "name": "Authorization", + "type": "apiKey", + "x-amazon-apigateway-authorizer": { + "providerARNs": [ + { + "Fn::GetAtt": "MyCognitoUserPool.Arn" + } + ], + "type": "cognito_user_pools" + }, + "x-amazon-apigateway-authtype": "cognito_user_pools" + } + }, "x-amazon-apigateway-gateway-responses": { "DEFAULT_5XX": { "responseParameters": { @@ -173,9 +182,9 @@ }, "Type": "AWS::ApiGateway::RestApi" }, - "ApiGatewayCognitoService15108F0BDeployment2a9725c838": { + "ApiGatewayCognitoService15108F0BDeployment46d69ebe56": { "Properties": { - "Description": "RestApi deployment id: 2a9725c838d10c88c6c75fec8e5fe7557ff62cea", + "Description": "RestApi deployment id: 46d69ebe56e878706dbfbf454b9cbdf8c15a5343", "RestApiId": { "Ref": "ApiGatewayCognitoService15108F0B" } @@ -185,7 +194,7 @@ "ApiGatewayCognitoService15108F0BprodStage": { "Properties": { "DeploymentId": { - "Ref": "ApiGatewayCognitoService15108F0BDeployment2a9725c838" + "Ref": "ApiGatewayCognitoService15108F0BDeployment46d69ebe56" }, "RestApiId": { "Ref": "ApiGatewayCognitoService15108F0B" diff --git a/tests/translator/output/aws-cn/feature_toggle_api_open_api_version_override.json b/tests/translator/output/aws-cn/feature_toggle_api_open_api_version_override.json new file mode 100644 index 000000000..2b1ece951 --- /dev/null +++ b/tests/translator/output/aws-cn/feature_toggle_api_open_api_version_override.json @@ -0,0 +1,273 @@ +{ + "Resources": { + "ApiGatewayCognitoExecutionRole4F7CB5C8": { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "apigateway.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": "lambda:Invoke*", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "LambdaFunction7804BD21", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "apigInvokeLambda" + } + ] + }, + "Type": "AWS::IAM::Role" + }, + "ApiGatewayCognitoService15108F0B": { + "Properties": { + "Body": { + "components": { + "schemas": { + "CreateReviewRequestContent": { + "properties": { + "reviewId": { + "type": "string" + } + }, + "type": "object" + }, + "CreateReviewResponseContent": { + "properties": { + "reviewId": { + "type": "string" + } + }, + "type": "object" + } + }, + "securitySchemes": { + "CognitoAuthorizer": { + "in": "header", + "name": "Authorization", + "type": "apiKey", + "x-amazon-apigateway-authorizer": { + "providerARNs": [ + { + "Fn::GetAtt": "MyCognitoUserPool.Arn" + } + ], + "type": "cognito_user_pools" + }, + "x-amazon-apigateway-authtype": "cognito_user_pools" + } + } + }, + "info": { + "title": "RxtHofApprovalServiceLambdaCognito", + "version": "2018-05-10" + }, + "openapi": "3.0.2", + "paths": { + "/reviews": { + "post": { + "operationId": "CreateReview", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateReviewRequestContent" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateReviewResponseContent" + } + } + }, + "description": "CreateReview 200 response", + "headers": { + "Access-Control-Allow-Origin": { + "schema": { + "type": "string" + } + }, + "Access-Control-Expose-Headers": { + "schema": { + "type": "string" + } + } + } + } + }, + "security": [ + { + "CognitoAuthorizer": [] + } + ], + "x-amazon-apigateway-integration": { + "credentials": { + "Fn::Sub": "${ApiGatewayCognitoExecutionRole4F7CB5C8.Arn}" + }, + "httpMethod": "POST", + "responses": { + "default": { + "responseParameters": { + "method.response.header.Access-Control-Allow-Origin": "'*'", + "method.response.header.Access-Control-Expose-Headers": "'Content-Length,Content-Type,X-Amzn-Errortype,X-Amzn-Requestid'" + }, + "statusCode": "200" + } + }, + "type": "aws_proxy", + "uri": { + "Fn::Sub": "arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${LambdaFunction7804BD21.Arn}/invocations" + } + } + } + } + }, + "security": [ + { + "aws.auth.sigv4": [] + } + ], + "x-amazon-apigateway-gateway-responses": { + "DEFAULT_5XX": { + "responseParameters": { + "gatewayresponse.header.Access-Control-Allow-Origin": "'*'" + }, + "responseTemplates": { + "application/json": "{\"message\":$context.error.messageString}" + } + } + } + }, + "EndpointConfiguration": { + "Types": [ + "REGIONAL" + ] + }, + "Parameters": { + "endpointConfigurationTypes": "REGIONAL" + } + }, + "Type": "AWS::ApiGateway::RestApi" + }, + "ApiGatewayCognitoService15108F0BDeployment2a9725c838": { + "Properties": { + "Description": "RestApi deployment id: 2a9725c838d10c88c6c75fec8e5fe7557ff62cea", + "RestApiId": { + "Ref": "ApiGatewayCognitoService15108F0B" + } + }, + "Type": "AWS::ApiGateway::Deployment" + }, + "ApiGatewayCognitoService15108F0BprodStage": { + "Properties": { + "DeploymentId": { + "Ref": "ApiGatewayCognitoService15108F0BDeployment2a9725c838" + }, + "RestApiId": { + "Ref": "ApiGatewayCognitoService15108F0B" + }, + "StageName": "prod", + "TracingEnabled": true + }, + "Type": "AWS::ApiGateway::Stage" + }, + "LambdaFunction7804BD21": { + "Properties": { + "Code": { + "ZipFile": "exports.handler = async (event, context, callback) => {\n const auth = event.queryStringParameters.authorization\n const policyDocument = {\n Version: '2012-10-17',\n Statement: [{\n Action: 'execute-api:Invoke',\n Effect: auth && auth.toLowerCase() === 'allow' ? 'Allow' : 'Deny',\n Resource: event.methodArn\n }]\n }\n \n return {\n principalId: 'user',\n context: {},\n policyDocument\n }\n}\n" + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "LambdaFunctionServiceRoleD6E423C9", + "Arn" + ] + }, + "Runtime": "nodejs16.x" + }, + "Type": "AWS::Lambda::Function" + }, + "LambdaFunctionServiceRoleD6E423C9": { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + }, + "Type": "AWS::IAM::Role" + }, + "LambdaFunctionServiceRoleDefaultPolicyF01A7EDC": { + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "sns:Publish", + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "LambdaFunctionServiceRoleDefaultPolicyF01A7EDC", + "Roles": [ + { + "Ref": "LambdaFunctionServiceRoleD6E423C9" + } + ] + }, + "Type": "AWS::IAM::Policy" + }, + "MyCognitoUserPool": { + "Properties": { + "UserPoolName": "MyCognitoUserPool" + }, + "Type": "AWS::Cognito::UserPool" + } + } +} diff --git a/tests/translator/output/aws-us-gov/api_open_api_version_override.json b/tests/translator/output/aws-us-gov/api_open_api_version_override.json index 2b1ece951..8ab3d795a 100644 --- a/tests/translator/output/aws-us-gov/api_open_api_version_override.json +++ b/tests/translator/output/aws-us-gov/api_open_api_version_override.json @@ -60,19 +60,12 @@ } }, "securitySchemes": { - "CognitoAuthorizer": { + "aws.auth.sigv4": { + "description": "AWS Signature Version 4 authentication", "in": "header", "name": "Authorization", "type": "apiKey", - "x-amazon-apigateway-authorizer": { - "providerARNs": [ - { - "Fn::GetAtt": "MyCognitoUserPool.Arn" - } - ], - "type": "cognito_user_pools" - }, - "x-amazon-apigateway-authtype": "cognito_user_pools" + "x-amazon-apigateway-authtype": "awsSigv4" } } }, @@ -151,6 +144,22 @@ "aws.auth.sigv4": [] } ], + "securityDefinitions": { + "CognitoAuthorizer": { + "in": "header", + "name": "Authorization", + "type": "apiKey", + "x-amazon-apigateway-authorizer": { + "providerARNs": [ + { + "Fn::GetAtt": "MyCognitoUserPool.Arn" + } + ], + "type": "cognito_user_pools" + }, + "x-amazon-apigateway-authtype": "cognito_user_pools" + } + }, "x-amazon-apigateway-gateway-responses": { "DEFAULT_5XX": { "responseParameters": { @@ -173,9 +182,9 @@ }, "Type": "AWS::ApiGateway::RestApi" }, - "ApiGatewayCognitoService15108F0BDeployment2a9725c838": { + "ApiGatewayCognitoService15108F0BDeployment46d69ebe56": { "Properties": { - "Description": "RestApi deployment id: 2a9725c838d10c88c6c75fec8e5fe7557ff62cea", + "Description": "RestApi deployment id: 46d69ebe56e878706dbfbf454b9cbdf8c15a5343", "RestApiId": { "Ref": "ApiGatewayCognitoService15108F0B" } @@ -185,7 +194,7 @@ "ApiGatewayCognitoService15108F0BprodStage": { "Properties": { "DeploymentId": { - "Ref": "ApiGatewayCognitoService15108F0BDeployment2a9725c838" + "Ref": "ApiGatewayCognitoService15108F0BDeployment46d69ebe56" }, "RestApiId": { "Ref": "ApiGatewayCognitoService15108F0B" diff --git a/tests/translator/output/aws-us-gov/feature_toggle_api_open_api_version_override.json b/tests/translator/output/aws-us-gov/feature_toggle_api_open_api_version_override.json new file mode 100644 index 000000000..2b1ece951 --- /dev/null +++ b/tests/translator/output/aws-us-gov/feature_toggle_api_open_api_version_override.json @@ -0,0 +1,273 @@ +{ + "Resources": { + "ApiGatewayCognitoExecutionRole4F7CB5C8": { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "apigateway.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": "lambda:Invoke*", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "LambdaFunction7804BD21", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "apigInvokeLambda" + } + ] + }, + "Type": "AWS::IAM::Role" + }, + "ApiGatewayCognitoService15108F0B": { + "Properties": { + "Body": { + "components": { + "schemas": { + "CreateReviewRequestContent": { + "properties": { + "reviewId": { + "type": "string" + } + }, + "type": "object" + }, + "CreateReviewResponseContent": { + "properties": { + "reviewId": { + "type": "string" + } + }, + "type": "object" + } + }, + "securitySchemes": { + "CognitoAuthorizer": { + "in": "header", + "name": "Authorization", + "type": "apiKey", + "x-amazon-apigateway-authorizer": { + "providerARNs": [ + { + "Fn::GetAtt": "MyCognitoUserPool.Arn" + } + ], + "type": "cognito_user_pools" + }, + "x-amazon-apigateway-authtype": "cognito_user_pools" + } + } + }, + "info": { + "title": "RxtHofApprovalServiceLambdaCognito", + "version": "2018-05-10" + }, + "openapi": "3.0.2", + "paths": { + "/reviews": { + "post": { + "operationId": "CreateReview", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateReviewRequestContent" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateReviewResponseContent" + } + } + }, + "description": "CreateReview 200 response", + "headers": { + "Access-Control-Allow-Origin": { + "schema": { + "type": "string" + } + }, + "Access-Control-Expose-Headers": { + "schema": { + "type": "string" + } + } + } + } + }, + "security": [ + { + "CognitoAuthorizer": [] + } + ], + "x-amazon-apigateway-integration": { + "credentials": { + "Fn::Sub": "${ApiGatewayCognitoExecutionRole4F7CB5C8.Arn}" + }, + "httpMethod": "POST", + "responses": { + "default": { + "responseParameters": { + "method.response.header.Access-Control-Allow-Origin": "'*'", + "method.response.header.Access-Control-Expose-Headers": "'Content-Length,Content-Type,X-Amzn-Errortype,X-Amzn-Requestid'" + }, + "statusCode": "200" + } + }, + "type": "aws_proxy", + "uri": { + "Fn::Sub": "arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${LambdaFunction7804BD21.Arn}/invocations" + } + } + } + } + }, + "security": [ + { + "aws.auth.sigv4": [] + } + ], + "x-amazon-apigateway-gateway-responses": { + "DEFAULT_5XX": { + "responseParameters": { + "gatewayresponse.header.Access-Control-Allow-Origin": "'*'" + }, + "responseTemplates": { + "application/json": "{\"message\":$context.error.messageString}" + } + } + } + }, + "EndpointConfiguration": { + "Types": [ + "REGIONAL" + ] + }, + "Parameters": { + "endpointConfigurationTypes": "REGIONAL" + } + }, + "Type": "AWS::ApiGateway::RestApi" + }, + "ApiGatewayCognitoService15108F0BDeployment2a9725c838": { + "Properties": { + "Description": "RestApi deployment id: 2a9725c838d10c88c6c75fec8e5fe7557ff62cea", + "RestApiId": { + "Ref": "ApiGatewayCognitoService15108F0B" + } + }, + "Type": "AWS::ApiGateway::Deployment" + }, + "ApiGatewayCognitoService15108F0BprodStage": { + "Properties": { + "DeploymentId": { + "Ref": "ApiGatewayCognitoService15108F0BDeployment2a9725c838" + }, + "RestApiId": { + "Ref": "ApiGatewayCognitoService15108F0B" + }, + "StageName": "prod", + "TracingEnabled": true + }, + "Type": "AWS::ApiGateway::Stage" + }, + "LambdaFunction7804BD21": { + "Properties": { + "Code": { + "ZipFile": "exports.handler = async (event, context, callback) => {\n const auth = event.queryStringParameters.authorization\n const policyDocument = {\n Version: '2012-10-17',\n Statement: [{\n Action: 'execute-api:Invoke',\n Effect: auth && auth.toLowerCase() === 'allow' ? 'Allow' : 'Deny',\n Resource: event.methodArn\n }]\n }\n \n return {\n principalId: 'user',\n context: {},\n policyDocument\n }\n}\n" + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "LambdaFunctionServiceRoleD6E423C9", + "Arn" + ] + }, + "Runtime": "nodejs16.x" + }, + "Type": "AWS::Lambda::Function" + }, + "LambdaFunctionServiceRoleD6E423C9": { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + }, + "Type": "AWS::IAM::Role" + }, + "LambdaFunctionServiceRoleDefaultPolicyF01A7EDC": { + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "sns:Publish", + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "LambdaFunctionServiceRoleDefaultPolicyF01A7EDC", + "Roles": [ + { + "Ref": "LambdaFunctionServiceRoleD6E423C9" + } + ] + }, + "Type": "AWS::IAM::Policy" + }, + "MyCognitoUserPool": { + "Properties": { + "UserPoolName": "MyCognitoUserPool" + }, + "Type": "AWS::Cognito::UserPool" + } + } +} diff --git a/tests/translator/output/feature_toggle_api_open_api_version_override.json b/tests/translator/output/feature_toggle_api_open_api_version_override.json new file mode 100644 index 000000000..90498cf57 --- /dev/null +++ b/tests/translator/output/feature_toggle_api_open_api_version_override.json @@ -0,0 +1,265 @@ +{ + "Resources": { + "ApiGatewayCognitoExecutionRole4F7CB5C8": { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "apigateway.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": "lambda:Invoke*", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "LambdaFunction7804BD21", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "apigInvokeLambda" + } + ] + }, + "Type": "AWS::IAM::Role" + }, + "ApiGatewayCognitoService15108F0B": { + "Properties": { + "Body": { + "components": { + "schemas": { + "CreateReviewRequestContent": { + "properties": { + "reviewId": { + "type": "string" + } + }, + "type": "object" + }, + "CreateReviewResponseContent": { + "properties": { + "reviewId": { + "type": "string" + } + }, + "type": "object" + } + }, + "securitySchemes": { + "CognitoAuthorizer": { + "in": "header", + "name": "Authorization", + "type": "apiKey", + "x-amazon-apigateway-authorizer": { + "providerARNs": [ + { + "Fn::GetAtt": "MyCognitoUserPool.Arn" + } + ], + "type": "cognito_user_pools" + }, + "x-amazon-apigateway-authtype": "cognito_user_pools" + } + } + }, + "info": { + "title": "RxtHofApprovalServiceLambdaCognito", + "version": "2018-05-10" + }, + "openapi": "3.0.2", + "paths": { + "/reviews": { + "post": { + "operationId": "CreateReview", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateReviewRequestContent" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateReviewResponseContent" + } + } + }, + "description": "CreateReview 200 response", + "headers": { + "Access-Control-Allow-Origin": { + "schema": { + "type": "string" + } + }, + "Access-Control-Expose-Headers": { + "schema": { + "type": "string" + } + } + } + } + }, + "security": [ + { + "CognitoAuthorizer": [] + } + ], + "x-amazon-apigateway-integration": { + "credentials": { + "Fn::Sub": "${ApiGatewayCognitoExecutionRole4F7CB5C8.Arn}" + }, + "httpMethod": "POST", + "responses": { + "default": { + "responseParameters": { + "method.response.header.Access-Control-Allow-Origin": "'*'", + "method.response.header.Access-Control-Expose-Headers": "'Content-Length,Content-Type,X-Amzn-Errortype,X-Amzn-Requestid'" + }, + "statusCode": "200" + } + }, + "type": "aws_proxy", + "uri": { + "Fn::Sub": "arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${LambdaFunction7804BD21.Arn}/invocations" + } + } + } + } + }, + "security": [ + { + "aws.auth.sigv4": [] + } + ], + "x-amazon-apigateway-gateway-responses": { + "DEFAULT_5XX": { + "responseParameters": { + "gatewayresponse.header.Access-Control-Allow-Origin": "'*'" + }, + "responseTemplates": { + "application/json": "{\"message\":$context.error.messageString}" + } + } + } + } + }, + "Type": "AWS::ApiGateway::RestApi" + }, + "ApiGatewayCognitoService15108F0BDeployment2a9725c838": { + "Properties": { + "Description": "RestApi deployment id: 2a9725c838d10c88c6c75fec8e5fe7557ff62cea", + "RestApiId": { + "Ref": "ApiGatewayCognitoService15108F0B" + } + }, + "Type": "AWS::ApiGateway::Deployment" + }, + "ApiGatewayCognitoService15108F0BprodStage": { + "Properties": { + "DeploymentId": { + "Ref": "ApiGatewayCognitoService15108F0BDeployment2a9725c838" + }, + "RestApiId": { + "Ref": "ApiGatewayCognitoService15108F0B" + }, + "StageName": "prod", + "TracingEnabled": true + }, + "Type": "AWS::ApiGateway::Stage" + }, + "LambdaFunction7804BD21": { + "Properties": { + "Code": { + "ZipFile": "exports.handler = async (event, context, callback) => {\n const auth = event.queryStringParameters.authorization\n const policyDocument = {\n Version: '2012-10-17',\n Statement: [{\n Action: 'execute-api:Invoke',\n Effect: auth && auth.toLowerCase() === 'allow' ? 'Allow' : 'Deny',\n Resource: event.methodArn\n }]\n }\n \n return {\n principalId: 'user',\n context: {},\n policyDocument\n }\n}\n" + }, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "LambdaFunctionServiceRoleD6E423C9", + "Arn" + ] + }, + "Runtime": "nodejs16.x" + }, + "Type": "AWS::Lambda::Function" + }, + "LambdaFunctionServiceRoleD6E423C9": { + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + }, + "Type": "AWS::IAM::Role" + }, + "LambdaFunctionServiceRoleDefaultPolicyF01A7EDC": { + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "sns:Publish", + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "LambdaFunctionServiceRoleDefaultPolicyF01A7EDC", + "Roles": [ + { + "Ref": "LambdaFunctionServiceRoleD6E423C9" + } + ] + }, + "Type": "AWS::IAM::Policy" + }, + "MyCognitoUserPool": { + "Properties": { + "UserPoolName": "MyCognitoUserPool" + }, + "Type": "AWS::Cognito::UserPool" + } + } +} diff --git a/tests/translator/test_translator.py b/tests/translator/test_translator.py index 326086aa8..2c5b8a211 100644 --- a/tests/translator/test_translator.py +++ b/tests/translator/test_translator.py @@ -34,9 +34,12 @@ BASE_PATH = os.path.dirname(__file__) INPUT_FOLDER = os.path.join(BASE_PATH, "input") SUCCESS_FILES_NAMES_FOR_TESTING = [ - os.path.splitext(f)[0] for f in os.listdir(INPUT_FOLDER) if not (f.startswith(("error_", "translate_"))) + os.path.splitext(f)[0] + for f in os.listdir(INPUT_FOLDER) + if not (f.startswith(("error_", "translate_"))) and not (f.startswith("feature_toggle_")) ] ERROR_FILES_NAMES_FOR_TESTING = [os.path.splitext(f)[0] for f in os.listdir(INPUT_FOLDER) if f.startswith("error_")] +FEATURE_TOGGLE_TESTING = [os.path.splitext(f)[0] for f in os.listdir(INPUT_FOLDER) if f.startswith("feature_toggle_")] OUTPUT_FOLDER = os.path.join(BASE_PATH, "output") @@ -154,7 +157,7 @@ def _read_expected_output(self, testcase, partition): expected_filepath = os.path.join(OUTPUT_FOLDER, partition_folder, testcase + ".json") return json.load(open(expected_filepath)) - def _compare_transform(self, manifest, expected, partition, region): + def _compare_transform(self, manifest, expected, partition, region, enable_feature_toggle=False): with patch("boto3.session.Session.region_name", region): parameter_values = get_template_parameter_values() mock_policy_loader = MagicMock() @@ -173,7 +176,12 @@ def _compare_transform(self, manifest, expected, partition, region): "AWSXRayDaemonWriteAccess" ] = f"arn:{partition}:iam::aws:policy/AWSXRayDaemonWriteAccess" - output_fragment = transform(manifest, parameter_values, mock_policy_loader) + if enable_feature_toggle: + mock_feature_toggle = MagicMock() + mock_feature_toggle.is_enabled.return_value = True + output_fragment = transform(manifest, parameter_values, mock_policy_loader, mock_feature_toggle) + else: + output_fragment = transform(manifest, parameter_values, mock_policy_loader) print(json.dumps(output_fragment, indent=2)) @@ -277,6 +285,31 @@ def test_transform_success(self, testcase, partition_with_region, mock_get_regio self._compare_transform(manifest, expected, partition, region) + @parameterized.expand( + itertools.product( + FEATURE_TOGGLE_TESTING, + [ + ("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("samtranslator.translator.arn_generator._get_region_from_session") + def test_transform_feature_toggle(self, testcase, partition_with_region, mock_get_region_from_session): + partition = partition_with_region[0] + region = partition_with_region[1] + mock_get_region_from_session.return_value = region + + manifest = self._read_input(testcase) + expected = self._read_expected_output(testcase, partition) + + self._compare_transform(manifest, expected, partition, region, True) + @parameterized.expand( itertools.product( [ From 6a7a89f663877bc38ebb27e910d275bdbd6f9c3f Mon Sep 17 00:00:00 2001 From: Xia Zhao <xazhao@amazon.com> Date: Mon, 4 Mar 2024 15:25:16 -0800 Subject: [PATCH 2/5] remove extra file --- .../input/feature_toggle_config.json | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/tests/feature_toggle/input/feature_toggle_config.json b/tests/feature_toggle/input/feature_toggle_config.json index 63f9f461c..d2d82ca3d 100644 --- a/tests/feature_toggle/input/feature_toggle_config.json +++ b/tests/feature_toggle/input/feature_toggle_config.json @@ -46,22 +46,5 @@ "enabled": false } } - }, - "normalized_open_api_version": { - "beta": { - "default": { - "enabled": false - } - }, - "gamma": { - "default": { - "enabled": false - } - }, - "prod": { - "default": { - "enabled": false - } - } } } From 7e891c2c5ffaedae2bb7a41e1be8f05ece3c289d Mon Sep 17 00:00:00 2001 From: Xia Zhao <xazhao@amazon.com> Date: Mon, 4 Mar 2024 15:31:21 -0800 Subject: [PATCH 3/5] create a static string for feature flag --- 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 e9b12461a..6ac0e8334 100644 --- a/samtranslator/model/api/api_generator.py +++ b/samtranslator/model/api/api_generator.py @@ -41,6 +41,8 @@ LOG = logging.getLogger(__name__) +FEATURE_FLAG_NORMALIZED_OPENAPI_VERSION = "normalized_open_api_version" + _CORS_WILDCARD = "'*'" CorsProperties = namedtuple( "CorsProperties", ["AllowMethods", "AllowHeaders", "AllowOrigin", "MaxAge", "AllowCredentials"] @@ -1127,7 +1129,7 @@ def _openapi_postprocess(self, definition_body: Dict[str, Any]) -> Dict[str, Any if definition_body.get("swagger") is not None: return definition_body - if self.feature_toggle and self.feature_toggle.is_enabled("normalized_open_api_version"): + if self.feature_toggle and self.feature_toggle.is_enabled(FEATURE_FLAG_NORMALIZED_OPENAPI_VERSION): normalized_open_api_version = definition_body.get("openapi", self.open_api_version) elif definition_body.get("openapi") is not None and self.open_api_version is None: normalized_open_api_version = definition_body.get("openapi") From 3128b377d7607a7eac6650485b52901a51f495c9 Mon Sep 17 00:00:00 2001 From: Xia Zhao <78883180+xazhao@users.noreply.github.com> Date: Fri, 19 Apr 2024 15:54:43 -0700 Subject: [PATCH 4/5] Update tests/translator/test_translator.py Co-authored-by: GZ <hz351086153@gmail.com> --- tests/translator/test_translator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/translator/test_translator.py b/tests/translator/test_translator.py index ec407279d..a0353d29e 100644 --- a/tests/translator/test_translator.py +++ b/tests/translator/test_translator.py @@ -36,7 +36,7 @@ SUCCESS_FILES_NAMES_FOR_TESTING = [ os.path.splitext(f)[0] for f in os.listdir(INPUT_FOLDER) - if not (f.startswith(("error_", "translate_"))) and not (f.startswith("feature_toggle_")) + if not (f.startswith(("error_", "translate_", "feature_toggle_"))) ] ERROR_FILES_NAMES_FOR_TESTING = [os.path.splitext(f)[0] for f in os.listdir(INPUT_FOLDER) if f.startswith("error_")] FEATURE_TOGGLE_TESTING = [os.path.splitext(f)[0] for f in os.listdir(INPUT_FOLDER) if f.startswith("feature_toggle_")] From f4f265ff95b134df9c852ee3f87ae749a58290e4 Mon Sep 17 00:00:00 2001 From: Xia Zhao <xazhao@amazon.com> Date: Fri, 19 Apr 2024 16:04:57 -0700 Subject: [PATCH 5/5] format --- tests/translator/test_translator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/translator/test_translator.py b/tests/translator/test_translator.py index a0353d29e..6090b1093 100644 --- a/tests/translator/test_translator.py +++ b/tests/translator/test_translator.py @@ -36,7 +36,7 @@ SUCCESS_FILES_NAMES_FOR_TESTING = [ os.path.splitext(f)[0] for f in os.listdir(INPUT_FOLDER) - if not (f.startswith(("error_", "translate_", "feature_toggle_"))) + if not (f.startswith(("error_", "translate_", "feature_toggle_"))) ] ERROR_FILES_NAMES_FOR_TESTING = [os.path.splitext(f)[0] for f in os.listdir(INPUT_FOLDER) if f.startswith("error_")] FEATURE_TOGGLE_TESTING = [os.path.splitext(f)[0] for f in os.listdir(INPUT_FOLDER) if f.startswith("feature_toggle_")]