From 8ae14aade8618d6f5039cdcdcc9c798c16d451ec Mon Sep 17 00:00:00 2001 From: Paul Cozzi Date: Tue, 23 Jul 2024 11:27:26 -0500 Subject: [PATCH] feat(type-safe-api): allow customized output spec bucket (#812) --- .../src/construct/type-safe-rest-api.ts | 16 +- .../type-safe-rest-api.test.ts.snap | 1458 +++++++++++++++++ .../test/construct/type-safe-rest-api.test.ts | 41 +- 3 files changed, 1511 insertions(+), 4 deletions(-) diff --git a/packages/type-safe-api/src/construct/type-safe-rest-api.ts b/packages/type-safe-api/src/construct/type-safe-rest-api.ts index 2b8ad66d7..35b56e98d 100644 --- a/packages/type-safe-api/src/construct/type-safe-rest-api.ts +++ b/packages/type-safe-api/src/construct/type-safe-rest-api.ts @@ -27,6 +27,7 @@ import { Runtime, } from "aws-cdk-lib/aws-lambda"; import { LogGroup } from "aws-cdk-lib/aws-logs"; +import { IBucket } from "aws-cdk-lib/aws-s3"; import { Asset } from "aws-cdk-lib/aws-s3-assets"; import { CfnIPSet, @@ -77,6 +78,12 @@ export interface TypeSafeRestApiProps * @default - Compression is disabled. */ readonly minCompressionSize?: Size; + + /** + * By default, the spec is prepared and outputted into the CDK assets bucket. If this is undesired, + * use this option to specify the output bucket. + */ + readonly outputSpecBucket?: IBucket; } /** @@ -113,6 +120,7 @@ export class TypeSafeRestApi extends Construct { operationLookup, defaultAuthorizer, corsOptions, + outputSpecBucket, ...options } = props; @@ -120,6 +128,8 @@ export class TypeSafeRestApi extends Construct { const inputSpecAsset = new Asset(this, "InputSpec", { path: specPath, }); + + const prepareSpecOutputBucket = outputSpecBucket ?? inputSpecAsset.bucket; // We'll output the prepared spec in the same asset bucket const preparedSpecOutputKeyPrefix = `${inputSpecAsset.s3ObjectKey}-prepared`; @@ -165,7 +175,7 @@ export class TypeSafeRestApi extends Construct { resources: [ // The output file will include a hash of the prepared spec, which is not known until deploy time since // tokens must be resolved - inputSpecAsset.bucket.arnForObjects( + prepareSpecOutputBucket.arnForObjects( `${preparedSpecOutputKeyPrefix}/*` ), ], @@ -340,7 +350,7 @@ export class TypeSafeRestApi extends Construct { key: inputSpecAsset.s3ObjectKey, }, outputSpecLocation: { - bucket: inputSpecAsset.bucket.bucketName, + bucket: prepareSpecOutputBucket.bucketName, key: preparedSpecOutputKeyPrefix, }, ...prepareSpecOptions, @@ -361,7 +371,7 @@ export class TypeSafeRestApi extends Construct { apiDefinition: this.node.tryGetContext("type-safe-api-local") ? ApiDefinition.fromInline(this.extendedApiSpecification) : ApiDefinition.fromBucket( - inputSpecAsset.bucket, + prepareSpecOutputBucket, prepareSpecCustomResource.getAttString("outputSpecKey") ), deployOptions: { diff --git a/packages/type-safe-api/test/construct/__snapshots__/type-safe-rest-api.test.ts.snap b/packages/type-safe-api/test/construct/__snapshots__/type-safe-rest-api.test.ts.snap index fb2748dbb..b463199e6 100644 --- a/packages/type-safe-api/test/construct/__snapshots__/type-safe-rest-api.test.ts.snap +++ b/packages/type-safe-api/test/construct/__snapshots__/type-safe-rest-api.test.ts.snap @@ -13069,6 +13069,1464 @@ exports[`Type Safe Rest Api Construct Unit Tests Synth With ApiKey Configuration } `; +exports[`Type Safe Rest Api Construct Unit Tests Synth with dedicated prepareSpecOutput bucket 1`] = ` +{ + "Mappings": { + "LatestNodeRuntimeMap": { + "af-south-1": { + "value": "nodejs20.x", + }, + "ap-east-1": { + "value": "nodejs20.x", + }, + "ap-northeast-1": { + "value": "nodejs20.x", + }, + "ap-northeast-2": { + "value": "nodejs20.x", + }, + "ap-northeast-3": { + "value": "nodejs20.x", + }, + "ap-south-1": { + "value": "nodejs20.x", + }, + "ap-south-2": { + "value": "nodejs20.x", + }, + "ap-southeast-1": { + "value": "nodejs20.x", + }, + "ap-southeast-2": { + "value": "nodejs20.x", + }, + "ap-southeast-3": { + "value": "nodejs20.x", + }, + "ap-southeast-4": { + "value": "nodejs20.x", + }, + "ca-central-1": { + "value": "nodejs20.x", + }, + "cn-north-1": { + "value": "nodejs18.x", + }, + "cn-northwest-1": { + "value": "nodejs18.x", + }, + "eu-central-1": { + "value": "nodejs20.x", + }, + "eu-central-2": { + "value": "nodejs20.x", + }, + "eu-north-1": { + "value": "nodejs20.x", + }, + "eu-south-1": { + "value": "nodejs20.x", + }, + "eu-south-2": { + "value": "nodejs20.x", + }, + "eu-west-1": { + "value": "nodejs20.x", + }, + "eu-west-2": { + "value": "nodejs20.x", + }, + "eu-west-3": { + "value": "nodejs20.x", + }, + "il-central-1": { + "value": "nodejs20.x", + }, + "me-central-1": { + "value": "nodejs20.x", + }, + "me-south-1": { + "value": "nodejs20.x", + }, + "sa-east-1": { + "value": "nodejs20.x", + }, + "us-east-1": { + "value": "nodejs20.x", + }, + "us-east-2": { + "value": "nodejs20.x", + }, + "us-gov-east-1": { + "value": "nodejs18.x", + }, + "us-gov-west-1": { + "value": "nodejs18.x", + }, + "us-iso-east-1": { + "value": "nodejs18.x", + }, + "us-iso-west-1": { + "value": "nodejs18.x", + }, + "us-isob-east-1": { + "value": "nodejs18.x", + }, + "us-west-1": { + "value": "nodejs20.x", + }, + "us-west-2": { + "value": "nodejs20.x", + }, + }, + }, + "Outputs": { + "ApiTestEndpoint34A72375": { + "Value": { + "Fn::Join": [ + "", + [ + "https://", + { + "Ref": "ApiTestEE73F324", + }, + ".execute-api.", + { + "Ref": "AWS::Region", + }, + ".", + { + "Ref": "AWS::URLSuffix", + }, + "/", + { + "Ref": "ApiTestDeploymentStageprod660267A6", + }, + "/", + ], + ], + }, + }, + }, + "Parameters": { + "BootstrapVersion": { + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]", + "Type": "AWS::SSM::Parameter::Value", + }, + }, + "Resources": { + "ApiTestAccessLogs92CFE051": { + "DeletionPolicy": "Retain", + "Metadata": { + "cdk_nag": { + "rules_to_suppress": [ + { + "applies_to": [ + { + "regex": "/^Policy::arn::iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs$/g", + }, + ], + "id": "AwsSolutions-IAM4", + "reason": "Cloudwatch Role requires access to create/read groups at the root level.", + }, + { + "applies_to": [ + { + "regex": "/^Policy::arn::iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs$/g", + }, + ], + "id": "AwsPrototyping-IAMNoManagedPolicies", + "reason": "Cloudwatch Role requires access to create/read groups at the root level.", + }, + { + "id": "AwsSolutions-APIG2", + "reason": "This construct implements fine grained validation via OpenApi.", + }, + { + "id": "AwsPrototyping-APIGWRequestValidation", + "reason": "This construct implements fine grained validation via OpenApi.", + }, + ], + }, + }, + "Properties": { + "RetentionInDays": 731, + }, + "Type": "AWS::Logs::LogGroup", + "UpdateReplacePolicy": "Retain", + }, + "ApiTestAccount272B5CDD": { + "DeletionPolicy": "Retain", + "DependsOn": [ + "ApiTestEE73F324", + "ApiTestPrepareSpecCustomResourceC9800EE6", + ], + "Metadata": { + "cdk_nag": { + "rules_to_suppress": [ + { + "applies_to": [ + { + "regex": "/^Policy::arn::iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs$/g", + }, + ], + "id": "AwsSolutions-IAM4", + "reason": "Cloudwatch Role requires access to create/read groups at the root level.", + }, + { + "applies_to": [ + { + "regex": "/^Policy::arn::iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs$/g", + }, + ], + "id": "AwsPrototyping-IAMNoManagedPolicies", + "reason": "Cloudwatch Role requires access to create/read groups at the root level.", + }, + { + "id": "AwsSolutions-APIG2", + "reason": "This construct implements fine grained validation via OpenApi.", + }, + { + "id": "AwsPrototyping-APIGWRequestValidation", + "reason": "This construct implements fine grained validation via OpenApi.", + }, + ], + }, + }, + "Properties": { + "CloudWatchRoleArn": { + "Fn::GetAtt": [ + "ApiTestCloudWatchRole56ED0814", + "Arn", + ], + }, + }, + "Type": "AWS::ApiGateway::Account", + "UpdateReplacePolicy": "Retain", + }, + "ApiTestApiTestAclApiWebACLA53F05F1": { + "Metadata": { + "cdk_nag": { + "rules_to_suppress": [ + { + "applies_to": [ + { + "regex": "/^Policy::arn::iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs$/g", + }, + ], + "id": "AwsSolutions-IAM4", + "reason": "Cloudwatch Role requires access to create/read groups at the root level.", + }, + { + "applies_to": [ + { + "regex": "/^Policy::arn::iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs$/g", + }, + ], + "id": "AwsPrototyping-IAMNoManagedPolicies", + "reason": "Cloudwatch Role requires access to create/read groups at the root level.", + }, + { + "id": "AwsSolutions-APIG2", + "reason": "This construct implements fine grained validation via OpenApi.", + }, + { + "id": "AwsPrototyping-APIGWRequestValidation", + "reason": "This construct implements fine grained validation via OpenApi.", + }, + ], + }, + }, + "Properties": { + "DefaultAction": { + "Allow": {}, + }, + "Name": "Default-ApiTest-Acl-b8f2cb4d", + "Rules": [ + { + "Name": "AWS-AWSManagedRulesCommonRuleSet", + "OverrideAction": { + "None": {}, + }, + "Priority": 2, + "Statement": { + "ManagedRuleGroupStatement": { + "Name": "AWSManagedRulesCommonRuleSet", + "VendorName": "AWS", + }, + }, + "VisibilityConfig": { + "CloudWatchMetricsEnabled": true, + "MetricName": "Default-ApiTest-Acl-b8f2cb4d-AWS-AWSManagedRulesCommonRuleSet", + "SampledRequestsEnabled": true, + }, + }, + ], + "Scope": "REGIONAL", + "VisibilityConfig": { + "CloudWatchMetricsEnabled": true, + "MetricName": "Default-ApiTest-Acl-b8f2cb4d", + "SampledRequestsEnabled": true, + }, + }, + "Type": "AWS::WAFv2::WebACL", + }, + "ApiTestApiTestAclWebACLAssociation54801610": { + "Metadata": { + "cdk_nag": { + "rules_to_suppress": [ + { + "applies_to": [ + { + "regex": "/^Policy::arn::iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs$/g", + }, + ], + "id": "AwsSolutions-IAM4", + "reason": "Cloudwatch Role requires access to create/read groups at the root level.", + }, + { + "applies_to": [ + { + "regex": "/^Policy::arn::iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs$/g", + }, + ], + "id": "AwsPrototyping-IAMNoManagedPolicies", + "reason": "Cloudwatch Role requires access to create/read groups at the root level.", + }, + { + "id": "AwsSolutions-APIG2", + "reason": "This construct implements fine grained validation via OpenApi.", + }, + { + "id": "AwsPrototyping-APIGWRequestValidation", + "reason": "This construct implements fine grained validation via OpenApi.", + }, + ], + }, + }, + "Properties": { + "ResourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition", + }, + ":apigateway:", + { + "Ref": "AWS::Region", + }, + "::/restapis/", + { + "Ref": "ApiTestEE73F324", + }, + "/stages/", + { + "Ref": "ApiTestDeploymentStageprod660267A6", + }, + ], + ], + }, + "WebACLArn": { + "Fn::GetAtt": [ + "ApiTestApiTestAclApiWebACLA53F05F1", + "Arn", + ], + }, + }, + "Type": "AWS::WAFv2::WebACLAssociation", + }, + "ApiTestCloudWatchRole56ED0814": { + "DeletionPolicy": "Retain", + "DependsOn": [ + "ApiTestPrepareSpecCustomResourceC9800EE6", + ], + "Metadata": { + "cdk_nag": { + "rules_to_suppress": [ + { + "applies_to": [ + { + "regex": "/^Policy::arn::iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs$/g", + }, + ], + "id": "AwsSolutions-IAM4", + "reason": "Cloudwatch Role requires access to create/read groups at the root level.", + }, + { + "applies_to": [ + { + "regex": "/^Policy::arn::iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs$/g", + }, + ], + "id": "AwsPrototyping-IAMNoManagedPolicies", + "reason": "Cloudwatch Role requires access to create/read groups at the root level.", + }, + { + "id": "AwsSolutions-APIG2", + "reason": "This construct implements fine grained validation via OpenApi.", + }, + { + "id": "AwsPrototyping-APIGWRequestValidation", + "reason": "This construct implements fine grained validation via OpenApi.", + }, + ], + }, + }, + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "apigateway.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition", + }, + ":iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs", + ], + ], + }, + ], + }, + "Type": "AWS::IAM::Role", + "UpdateReplacePolicy": "Retain", + }, + "ApiTestDeployment153EC47805274f2aa1dcdb58d4d7f9ef6a0c2ecf": { + "DependsOn": [ + "ApiTestPrepareSpecCustomResourceC9800EE6", + ], + "Metadata": { + "cdk_nag": { + "rules_to_suppress": [ + { + "applies_to": [ + { + "regex": "/^Policy::arn::iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs$/g", + }, + ], + "id": "AwsSolutions-IAM4", + "reason": "Cloudwatch Role requires access to create/read groups at the root level.", + }, + { + "applies_to": [ + { + "regex": "/^Policy::arn::iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs$/g", + }, + ], + "id": "AwsPrototyping-IAMNoManagedPolicies", + "reason": "Cloudwatch Role requires access to create/read groups at the root level.", + }, + { + "id": "AwsSolutions-APIG2", + "reason": "This construct implements fine grained validation via OpenApi.", + }, + { + "id": "AwsPrototyping-APIGWRequestValidation", + "reason": "This construct implements fine grained validation via OpenApi.", + }, + ], + }, + }, + "Properties": { + "Description": "Automatically created by the RestApi construct", + "RestApiId": { + "Ref": "ApiTestEE73F324", + }, + }, + "Type": "AWS::ApiGateway::Deployment", + }, + "ApiTestDeploymentStageprod660267A6": { + "DependsOn": [ + "ApiTestAccount272B5CDD", + "ApiTestPrepareSpecCustomResourceC9800EE6", + ], + "Metadata": { + "cdk_nag": { + "rules_to_suppress": [ + { + "applies_to": [ + { + "regex": "/^Policy::arn::iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs$/g", + }, + ], + "id": "AwsSolutions-IAM4", + "reason": "Cloudwatch Role requires access to create/read groups at the root level.", + }, + { + "applies_to": [ + { + "regex": "/^Policy::arn::iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs$/g", + }, + ], + "id": "AwsPrototyping-IAMNoManagedPolicies", + "reason": "Cloudwatch Role requires access to create/read groups at the root level.", + }, + { + "id": "AwsSolutions-APIG2", + "reason": "This construct implements fine grained validation via OpenApi.", + }, + { + "id": "AwsPrototyping-APIGWRequestValidation", + "reason": "This construct implements fine grained validation via OpenApi.", + }, + ], + }, + }, + "Properties": { + "AccessLogSetting": { + "DestinationArn": { + "Fn::GetAtt": [ + "ApiTestAccessLogs92CFE051", + "Arn", + ], + }, + "Format": "$context.identity.sourceIp $context.identity.caller $context.identity.user [$context.requestTime] "$context.httpMethod $context.resourcePath $context.protocol" $context.status $context.responseLength $context.requestId", + }, + "DeploymentId": { + "Ref": "ApiTestDeployment153EC47805274f2aa1dcdb58d4d7f9ef6a0c2ecf", + }, + "MethodSettings": [ + { + "DataTraceEnabled": false, + "HttpMethod": "*", + "LoggingLevel": "INFO", + "ResourcePath": "/*", + }, + ], + "RestApiId": { + "Ref": "ApiTestEE73F324", + }, + "StageName": "prod", + }, + "Type": "AWS::ApiGateway::Stage", + }, + "ApiTestEE73F324": { + "DependsOn": [ + "ApiTestPrepareSpecCustomResourceC9800EE6", + ], + "Metadata": { + "cdk_nag": { + "rules_to_suppress": [ + { + "applies_to": [ + { + "regex": "/^Policy::arn::iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs$/g", + }, + ], + "id": "AwsSolutions-IAM4", + "reason": "Cloudwatch Role requires access to create/read groups at the root level.", + }, + { + "applies_to": [ + { + "regex": "/^Policy::arn::iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs$/g", + }, + ], + "id": "AwsPrototyping-IAMNoManagedPolicies", + "reason": "Cloudwatch Role requires access to create/read groups at the root level.", + }, + { + "id": "AwsSolutions-APIG2", + "reason": "This construct implements fine grained validation via OpenApi.", + }, + { + "id": "AwsPrototyping-APIGWRequestValidation", + "reason": "This construct implements fine grained validation via OpenApi.", + }, + ], + }, + }, + "Properties": { + "BodyS3Location": { + "Bucket": { + "Ref": "PrepareSpecBucket95FB609F", + }, + "Key": { + "Fn::GetAtt": [ + "ApiTestPrepareSpecCustomResourceC9800EE6", + "outputSpecKey", + ], + }, + }, + "Name": "ApiTest", + }, + "Type": "AWS::ApiGateway::RestApi", + }, + "ApiTestLambdaPermissiontestOperationECAC1A2D": { + "Metadata": { + "cdk_nag": { + "rules_to_suppress": [ + { + "applies_to": [ + { + "regex": "/^Policy::arn::iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs$/g", + }, + ], + "id": "AwsSolutions-IAM4", + "reason": "Cloudwatch Role requires access to create/read groups at the root level.", + }, + { + "applies_to": [ + { + "regex": "/^Policy::arn::iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs$/g", + }, + ], + "id": "AwsPrototyping-IAMNoManagedPolicies", + "reason": "Cloudwatch Role requires access to create/read groups at the root level.", + }, + { + "id": "AwsSolutions-APIG2", + "reason": "This construct implements fine grained validation via OpenApi.", + }, + { + "id": "AwsPrototyping-APIGWRequestValidation", + "reason": "This construct implements fine grained validation via OpenApi.", + }, + ], + }, + }, + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "LambdaD247545B", + "Arn", + ], + }, + "Principal": "apigateway.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition", + }, + ":execute-api:", + { + "Ref": "AWS::Region", + }, + ":", + { + "Ref": "AWS::AccountId", + }, + ":", + { + "Ref": "ApiTestEE73F324", + }, + "/*/GET/test", + ], + ], + }, + }, + "Type": "AWS::Lambda::Permission", + }, + "ApiTestPrepareSpecCustomResourceC9800EE6": { + "DeletionPolicy": "Delete", + "Metadata": { + "cdk_nag": { + "rules_to_suppress": [ + { + "applies_to": [ + { + "regex": "/^Policy::arn::iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs$/g", + }, + ], + "id": "AwsSolutions-IAM4", + "reason": "Cloudwatch Role requires access to create/read groups at the root level.", + }, + { + "applies_to": [ + { + "regex": "/^Policy::arn::iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs$/g", + }, + ], + "id": "AwsPrototyping-IAMNoManagedPolicies", + "reason": "Cloudwatch Role requires access to create/read groups at the root level.", + }, + { + "id": "AwsSolutions-APIG2", + "reason": "This construct implements fine grained validation via OpenApi.", + }, + { + "id": "AwsPrototyping-APIGWRequestValidation", + "reason": "This construct implements fine grained validation via OpenApi.", + }, + ], + }, + }, + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "ApiTestPrepareSpecProviderframeworkonEvent2FA9E188", + "Arn", + ], + }, + "inputSpecLocation": { + "bucket": { + "Fn::Sub": "cdk-hnb659fds-assets-\${AWS::AccountId}-\${AWS::Region}", + }, + "key": "ec22714a0fde30e0834df19bc639f3cb3519abd3ccd6dcf6b761d105827ca227.json", + }, + "integrations": { + "testOperation": { + "integration": { + "httpMethod": "POST", + "passthroughBehavior": "WHEN_NO_MATCH", + "type": "AWS_PROXY", + "uri": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition", + }, + ":apigateway:", + { + "Ref": "AWS::Region", + }, + ":lambda:path/2015-03-31/functions/", + { + "Fn::GetAtt": [ + "LambdaD247545B", + "Arn", + ], + }, + "/invocations", + ], + ], + }, + }, + }, + }, + "operationLookup": { + "testOperation": { + "method": "get", + "path": "/test", + }, + }, + "outputSpecLocation": { + "bucket": { + "Ref": "PrepareSpecBucket95FB609F", + }, + "key": "ec22714a0fde30e0834df19bc639f3cb3519abd3ccd6dcf6b761d105827ca227.json-prepared", + }, + "securitySchemes": {}, + }, + "Type": "AWS::CloudFormation::CustomResource", + "UpdateReplacePolicy": "Delete", + }, + "ApiTestPrepareSpecHandler46C6FEB5": { + "DependsOn": [ + "ApiTestPrepareSpecRole44D562E5", + ], + "Metadata": { + "cdk_nag": { + "rules_to_suppress": [ + { + "applies_to": [ + { + "regex": "/^Policy::arn::iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs$/g", + }, + ], + "id": "AwsSolutions-IAM4", + "reason": "Cloudwatch Role requires access to create/read groups at the root level.", + }, + { + "applies_to": [ + { + "regex": "/^Policy::arn::iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs$/g", + }, + ], + "id": "AwsPrototyping-IAMNoManagedPolicies", + "reason": "Cloudwatch Role requires access to create/read groups at the root level.", + }, + { + "id": "AwsSolutions-APIG2", + "reason": "This construct implements fine grained validation via OpenApi.", + }, + { + "id": "AwsPrototyping-APIGWRequestValidation", + "reason": "This construct implements fine grained validation via OpenApi.", + }, + ], + }, + }, + "Properties": { + "Code": { + "S3Bucket": { + "Fn::Sub": "cdk-hnb659fds-assets-\${AWS::AccountId}-\${AWS::Region}", + }, + "S3Key": "8adf3c609b6d73fa935d9c361e37b816f2da1d59567bf9c753fc8319e6a58e7f.zip", + }, + "FunctionName": "Default-3E755E54PrepSpec", + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "ApiTestPrepareSpecRole44D562E5", + "Arn", + ], + }, + "Runtime": "nodejs18.x", + "Timeout": 30, + }, + "Type": "AWS::Lambda::Function", + }, + "ApiTestPrepareSpecProviderRoleDefaultPolicy99662E78": { + "Metadata": { + "cdk_nag": { + "rules_to_suppress": [ + { + "id": "AwsSolutions-IAM5", + "reason": "Cloudwatch resources have been scoped down to the LogGroup level, however * is still needed as stream names are created just in time.", + }, + { + "id": "AwsPrototyping-IAMNoWildcardPermissions", + "reason": "Cloudwatch resources have been scoped down to the LogGroup level, however * is still needed as stream names are created just in time.", + }, + { + "applies_to": [ + { + "regex": "/^Policy::arn::iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs$/g", + }, + ], + "id": "AwsSolutions-IAM4", + "reason": "Cloudwatch Role requires access to create/read groups at the root level.", + }, + { + "applies_to": [ + { + "regex": "/^Policy::arn::iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs$/g", + }, + ], + "id": "AwsPrototyping-IAMNoManagedPolicies", + "reason": "Cloudwatch Role requires access to create/read groups at the root level.", + }, + { + "id": "AwsSolutions-APIG2", + "reason": "This construct implements fine grained validation via OpenApi.", + }, + { + "id": "AwsPrototyping-APIGWRequestValidation", + "reason": "This construct implements fine grained validation via OpenApi.", + }, + ], + }, + }, + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "lambda:InvokeFunction", + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "ApiTestPrepareSpecHandler46C6FEB5", + "Arn", + ], + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "ApiTestPrepareSpecHandler46C6FEB5", + "Arn", + ], + }, + ":*", + ], + ], + }, + ], + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "ApiTestPrepareSpecProviderRoleDefaultPolicy99662E78", + "Roles": [ + { + "Ref": "ApiTestPrepareSpecProviderRoleF47822B8", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, + "ApiTestPrepareSpecProviderRoleF47822B8": { + "Metadata": { + "cdk_nag": { + "rules_to_suppress": [ + { + "id": "AwsSolutions-IAM5", + "reason": "Cloudwatch resources have been scoped down to the LogGroup level, however * is still needed as stream names are created just in time.", + }, + { + "id": "AwsPrototyping-IAMNoWildcardPermissions", + "reason": "Cloudwatch resources have been scoped down to the LogGroup level, however * is still needed as stream names are created just in time.", + }, + { + "applies_to": [ + { + "regex": "/^Policy::arn::iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs$/g", + }, + ], + "id": "AwsSolutions-IAM4", + "reason": "Cloudwatch Role requires access to create/read groups at the root level.", + }, + { + "applies_to": [ + { + "regex": "/^Policy::arn::iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs$/g", + }, + ], + "id": "AwsPrototyping-IAMNoManagedPolicies", + "reason": "Cloudwatch Role requires access to create/read groups at the root level.", + }, + { + "id": "AwsSolutions-APIG2", + "reason": "This construct implements fine grained validation via OpenApi.", + }, + { + "id": "AwsPrototyping-APIGWRequestValidation", + "reason": "This construct implements fine grained validation via OpenApi.", + }, + ], + }, + }, + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents", + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn:aws:logs:", + { + "Ref": "AWS::Region", + }, + ":", + { + "Ref": "AWS::AccountId", + }, + ":log-group:/aws/lambda/Default-3E755E54PrepSpecProvider", + ], + ], + }, + { + "Fn::Join": [ + "", + [ + "arn:aws:logs:", + { + "Ref": "AWS::Region", + }, + ":", + { + "Ref": "AWS::AccountId", + }, + ":log-group:/aws/lambda/Default-3E755E54PrepSpecProvider:*", + ], + ], + }, + ], + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "logs", + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + "ApiTestPrepareSpecProviderframeworkonEvent2FA9E188": { + "DependsOn": [ + "ApiTestPrepareSpecProviderRoleDefaultPolicy99662E78", + "ApiTestPrepareSpecProviderRoleF47822B8", + ], + "Metadata": { + "cdk_nag": { + "rules_to_suppress": [ + { + "id": "AwsSolutions-L1", + "reason": "Latest runtime cannot be configured. CDK will need to upgrade the Provider construct accordingly.", + }, + { + "id": "AwsPrototyping-LambdaLatestVersion", + "reason": "Latest runtime cannot be configured. CDK will need to upgrade the Provider construct accordingly.", + }, + { + "applies_to": [ + { + "regex": "/^Policy::arn::iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs$/g", + }, + ], + "id": "AwsSolutions-IAM4", + "reason": "Cloudwatch Role requires access to create/read groups at the root level.", + }, + { + "applies_to": [ + { + "regex": "/^Policy::arn::iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs$/g", + }, + ], + "id": "AwsPrototyping-IAMNoManagedPolicies", + "reason": "Cloudwatch Role requires access to create/read groups at the root level.", + }, + { + "id": "AwsSolutions-APIG2", + "reason": "This construct implements fine grained validation via OpenApi.", + }, + { + "id": "AwsPrototyping-APIGWRequestValidation", + "reason": "This construct implements fine grained validation via OpenApi.", + }, + ], + }, + }, + "Properties": { + "Code": { + "S3Bucket": { + "Fn::Sub": "cdk-hnb659fds-assets-\${AWS::AccountId}-\${AWS::Region}", + }, + "S3Key": "3542be390685e0c8353d92ccb5796d343cd93ca946b6b0de798004206a199adc.zip", + }, + "Description": "AWS CDK resource provider framework - onEvent (Default/ApiTest/PrepareSpecProvider)", + "Environment": { + "Variables": { + "USER_ON_EVENT_FUNCTION_ARN": { + "Fn::GetAtt": [ + "ApiTestPrepareSpecHandler46C6FEB5", + "Arn", + ], + }, + }, + }, + "FunctionName": "Default-3E755E54PrepSpecProvider", + "Handler": "framework.onEvent", + "Role": { + "Fn::GetAtt": [ + "ApiTestPrepareSpecProviderRoleF47822B8", + "Arn", + ], + }, + "Runtime": { + "Fn::FindInMap": [ + "LatestNodeRuntimeMap", + { + "Ref": "AWS::Region", + }, + "value", + ], + }, + "Timeout": 900, + }, + "Type": "AWS::Lambda::Function", + }, + "ApiTestPrepareSpecRole44D562E5": { + "Metadata": { + "cdk_nag": { + "rules_to_suppress": [ + { + "applies_to": [ + { + "regex": "/^Resource::arn:aws:logs:::log-group:/aws/lambda/Default-3E755E54PrepSpec:*/g", + }, + ], + "id": "AwsSolutions-IAM5", + "reason": "Cloudwatch resources have been scoped down to the LogGroup level, however * is still needed as stream names are created just in time.", + }, + { + "applies_to": [ + { + "regex": "/^Resource::arn::s3:.*/ec22714a0fde30e0834df19bc639f3cb3519abd3ccd6dcf6b761d105827ca227.json-prepared/*/g", + }, + ], + "id": "AwsSolutions-IAM5", + "reason": "S3 resources have been scoped down to the appropriate prefix in the CDK asset bucket, however * is still needed as since the prepared spec hash is not known until deploy time.", + }, + { + "applies_to": [ + { + "regex": "/^Resource::arn:aws:logs:::log-group:/aws/lambda/Default-3E755E54PrepSpec:*/g", + }, + ], + "id": "AwsPrototyping-IAMNoWildcardPermissions", + "reason": "Cloudwatch resources have been scoped down to the LogGroup level, however * is still needed as stream names are created just in time.", + }, + { + "applies_to": [ + { + "regex": "/^Resource::arn::s3:.*/ec22714a0fde30e0834df19bc639f3cb3519abd3ccd6dcf6b761d105827ca227.json-prepared/*/g", + }, + ], + "id": "AwsPrototyping-IAMNoWildcardPermissions", + "reason": "S3 resources have been scoped down to the appropriate prefix in the CDK asset bucket, however * is still needed as since the prepared spec hash is not known until deploy time.", + }, + { + "applies_to": [ + { + "regex": "/^Policy::arn::iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs$/g", + }, + ], + "id": "AwsSolutions-IAM4", + "reason": "Cloudwatch Role requires access to create/read groups at the root level.", + }, + { + "applies_to": [ + { + "regex": "/^Policy::arn::iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs$/g", + }, + ], + "id": "AwsPrototyping-IAMNoManagedPolicies", + "reason": "Cloudwatch Role requires access to create/read groups at the root level.", + }, + { + "id": "AwsSolutions-APIG2", + "reason": "This construct implements fine grained validation via OpenApi.", + }, + { + "id": "AwsPrototyping-APIGWRequestValidation", + "reason": "This construct implements fine grained validation via OpenApi.", + }, + ], + }, + }, + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com", + }, + }, + ], + "Version": "2012-10-17", + }, + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents", + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn:aws:logs:", + { + "Ref": "AWS::Region", + }, + ":", + { + "Ref": "AWS::AccountId", + }, + ":log-group:/aws/lambda/Default-3E755E54PrepSpec", + ], + ], + }, + { + "Fn::Join": [ + "", + [ + "arn:aws:logs:", + { + "Ref": "AWS::Region", + }, + ":", + { + "Ref": "AWS::AccountId", + }, + ":log-group:/aws/lambda/Default-3E755E54PrepSpec:*", + ], + ], + }, + ], + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "logs", + }, + { + "PolicyDocument": { + "Statement": [ + { + "Action": "s3:getObject", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition", + }, + ":s3:::", + { + "Fn::Sub": "cdk-hnb659fds-assets-\${AWS::AccountId}-\${AWS::Region}", + }, + "/ec22714a0fde30e0834df19bc639f3cb3519abd3ccd6dcf6b761d105827ca227.json", + ], + ], + }, + }, + { + "Action": "s3:putObject", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "PrepareSpecBucket95FB609F", + "Arn", + ], + }, + "/ec22714a0fde30e0834df19bc639f3cb3519abd3ccd6dcf6b761d105827ca227.json-prepared/*", + ], + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "s3", + }, + ], + }, + "Type": "AWS::IAM::Role", + }, + "LambdaD247545B": { + "DependsOn": [ + "LambdaServiceRoleA8ED4D3B", + ], + "Metadata": { + "cdk_nag": { + "rules_to_suppress": [ + { + "id": "AwsSolutions-IAM4", + "reason": "This is a test construct.", + }, + { + "id": "AwsPrototyping-IAMNoManagedPolicies", + "reason": "This is a test construct.", + }, + ], + }, + }, + "Properties": { + "Code": { + "ZipFile": "code", + }, + "Handler": "handler", + "Role": { + "Fn::GetAtt": [ + "LambdaServiceRoleA8ED4D3B", + "Arn", + ], + }, + "Runtime": "nodejs16.x", + }, + "Type": "AWS::Lambda::Function", + }, + "LambdaServiceRoleA8ED4D3B": { + "Metadata": { + "cdk_nag": { + "rules_to_suppress": [ + { + "id": "AwsSolutions-IAM4", + "reason": "This is a test construct.", + }, + { + "id": "AwsPrototyping-IAMNoManagedPolicies", + "reason": "This is a test construct.", + }, + ], + }, + }, + "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", + }, + "PrepareSpecBucket95FB609F": { + "DeletionPolicy": "Retain", + "Properties": { + "PublicAccessBlockConfiguration": { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true, + }, + }, + "Type": "AWS::S3::Bucket", + "UpdateReplacePolicy": "Retain", + }, + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5", + ], + { + "Ref": "BootstrapVersion", + }, + ], + }, + ], + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI.", + }, + ], + }, + }, +} +`; + +exports[`Type Safe Rest Api Construct Unit Tests Synth with dedicated prepareSpecOutput bucket 2`] = ` +{ + "components": { + "securitySchemes": {}, + }, + "info": { + "title": "Test API", + "version": "1.0.0", + }, + "openapi": "3.0.3", + "paths": { + "/test": { + "get": { + "operationId": "testOperation", + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "properties": { + "message": { + "type": "string", + }, + }, + "type": "object", + }, + }, + }, + "description": "Successful response", + "headers": {}, + }, + }, + "x-amazon-apigateway-integration": { + "httpMethod": "POST", + "passthroughBehavior": "WHEN_NO_MATCH", + "type": "AWS_PROXY", + "uri": "arn:\${}:apigateway:\${}:lambda:path/2015-03-31/functions/\${}/invocations", + }, + }, + }, + }, + "x-amazon-apigateway-gateway-responses": { + "BAD_REQUEST_BODY": { + "responseTemplates": { + "application/json": "{"message": "$context.error.validationErrorString"}", + }, + "statusCode": 400, + }, + }, + "x-amazon-apigateway-request-validator": "all", + "x-amazon-apigateway-request-validators": { + "all": { + "validateRequestBody": true, + "validateRequestParameters": true, + }, + }, +} +`; + exports[`Type Safe Rest Api Construct Unit Tests With Cognito Auth 1`] = ` { "Mappings": { diff --git a/packages/type-safe-api/test/construct/type-safe-rest-api.test.ts b/packages/type-safe-api/test/construct/type-safe-rest-api.test.ts index b94f13a05..46dfe4a32 100644 --- a/packages/type-safe-api/test/construct/type-safe-rest-api.test.ts +++ b/packages/type-safe-api/test/construct/type-safe-rest-api.test.ts @@ -6,7 +6,7 @@ import { Template } from "aws-cdk-lib/assertions"; import { ApiKeySourceType, Cors } from "aws-cdk-lib/aws-apigateway"; import { UserPool } from "aws-cdk-lib/aws-cognito"; import { Code, Function, Runtime } from "aws-cdk-lib/aws-lambda"; -import { Bucket } from "aws-cdk-lib/aws-s3"; +import { BlockPublicAccess, Bucket } from "aws-cdk-lib/aws-s3"; import { NagSuppressions } from "cdk-nag"; import * as _ from "lodash"; import { OpenAPIV3 } from "openapi-types"; @@ -161,6 +161,45 @@ describe("Type Safe Rest Api Construct Unit Tests", () => { }); }); + it("Synth with dedicated prepareSpecOutput bucket", () => { + const stack = new Stack(PDKNag.app()); + const func = new Function(stack, "Lambda", { + code: Code.fromInline("code"), + handler: "handler", + runtime: Runtime.NODEJS_16_X, + }); + withTempSpec(sampleSpec, (specPath) => { + const api = new TypeSafeRestApi(stack, "ApiTest", { + specPath, + operationLookup, + integrations: { + testOperation: { + integration: Integrations.lambda(func), + }, + }, + outputSpecBucket: new Bucket(stack, "PrepareSpecBucket", { + blockPublicAccess: BlockPublicAccess.BLOCK_ALL, + }), + }); + ["AwsSolutions-IAM4", "AwsPrototyping-IAMNoManagedPolicies"].forEach( + (RuleId) => { + NagSuppressions.addResourceSuppressions( + func, + [ + { + id: RuleId, + reason: "This is a test construct.", + }, + ], + true + ); + } + ); + expect(Template.fromStack(stack).toJSON()).toMatchSnapshot(); + snapshotExtendedSpec(api); + }); + }); + it("Create 2 APIs on same stack", () => { const stack = new Stack(PDKNag.app()); const func = new Function(stack, "Lambda", {