diff --git a/.gitignore b/.gitignore index c49eecd99..05881a558 100755 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ # dependencies **/node_modules +**/modules # test assets **/coverage diff --git a/CHANGELOG.md b/CHANGELOG.md index 83a91ac3f..52d8ef893 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,12 +5,44 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [6.2.5] - 2024-01-12 +## [6.2.6] - 2024-06-27 + +### Added +- StackId tag to CloudFrontLoggingBucket and its bucket name as a CfnOutput [#529](https://github.com/aws-solutions/serverless-image-handler/issues/529) +- Test case to verify UTF-8 support in object key [#320](https://github.com/aws-solutions/serverless-image-handler/pull/320) +- Test cases to verify crop functionality [#459](https://github.com/aws-solutions/serverless-image-handler/pull/459) +- VERSION.txt and build script change to auto-update local package versions +- S3:bucket-name tag for defining which source bucket to use in thumbor style requests [#521](https://github.com/aws-solutions/serverless-image-handler/pull/521) +- Ability to override whether an image should be animated [#456](https://github.com/aws-solutions/serverless-image-handler/issues/456) +- Support for 8-bit depth AVIF image type inference [#360](https://github.com/aws-solutions/serverless-image-handler/issues/360) + +### Changed +- Decreased permissions allotted to CustomResource Lambda and ImageHandler Lambda +- cdk update to 2.124.0 +- aws-solutions-constructs update to 2.51.0 +- SourceBucketsParameter to require explicit bucket names +- Demo-ui dependency update +- Demo-ui to be a package and manage script/stylesheet dependencies through NPM +- Modified JPEG SOI marker parsing to only check first 2 bytes [#429] + +### Security +- Upgraded follow-redirects to v1.15.6 for vulnerability CVE-2024-28849 +- Upgraded braces to v3.0.3 for vulnerability CVE-2024-4068 + +### Removed +- Unused CopyS3Assets custom resource + +### Fixed +- Some error messages indicating incorrect file types +- Solution version and id not being passed to Backend Lambda +- Thumbor-style URL matching being overly permissive + + +## [6.2.5] - 2024-01-03 ### Fixed - Ensure accurate image metadata when generating Amazon Rekognition compatible images [#374](https://github.com/aws-solutions/serverless-image-handler/issues/374) -- Upgraded axios to v1.6.5 for vulnerability CVE-2023-26159 - Exclude demo-ui-config from being deleted upon BucketDeployment update sync when updating to a new version ### Changed @@ -20,6 +52,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - typescript update to 5.3.3 - GIF files without multiple pages are now treated as non-animated, allowing all filters to be used on them [#460](https://github.com/aws-solutions/serverless-image-handler/issues/460) +### Security + +- Upgraded axios to v1.6.5 for vulnerability CVE-2023-26159 + ## [6.2.4] - 2023-12-06 ### Changed diff --git a/README.md b/README.md index 5fa3cdd2d..56b417c67 100644 --- a/README.md +++ b/README.md @@ -105,6 +105,8 @@ This solution collects anonymous operational metrics to help AWS improve the qua - [@Fjool](https://github.com/Fjool) for [#489](https://github.com/aws-solutions/serverless-image-handler/pull/489) - [@fvsnippets](https://github.com/fvsnippets) for [#373](https://github.com/aws-solutions/serverless-image-handler/pull/373), [#380](https://github.com/aws-solutions/serverless-image-handler/pull/380) - [@ccchapman](https://github.com/ccchapman) for [#490](https://github.com/aws-solutions/serverless-image-handler/pull/490) +- [@bennet-esyoil][https://github.com/bennet-esyoil] for [#521](https://github.com/aws-solutions/serverless-image-handler/pull/521) +- [@vaniyokk][https://github.com/vaniyokk] for [#511](https://github.com/aws-solutions/serverless-image-handler/pull/511) # License diff --git a/VERSION.txt b/VERSION.txt new file mode 100644 index 000000000..417c02022 --- /dev/null +++ b/VERSION.txt @@ -0,0 +1 @@ +6.2.6 \ No newline at end of file diff --git a/deployment/build-s3-dist.sh b/deployment/build-s3-dist.sh index e37653a4b..c223eb6b1 100755 --- a/deployment/build-s3-dist.sh +++ b/deployment/build-s3-dist.sh @@ -32,6 +32,9 @@ mkdir -p "$template_dist_dir" rm -rf "$build_dist_dir" mkdir -p "$build_dist_dir" +headline "[Init] Ensure package versions are updated" +npm --prefix "$source_dir" run bump-version + headline "[Build] Synthesize cdk template and assets" cd "$cdk_source_dir" npm run clean:install diff --git a/deployment/cdk-solution-helper/package-lock.json b/deployment/cdk-solution-helper/package-lock.json index eb7b554b4..f8b88f293 100644 --- a/deployment/cdk-solution-helper/package-lock.json +++ b/deployment/cdk-solution-helper/package-lock.json @@ -10,7 +10,7 @@ "license": "Apache-2.0", "dependencies": { "adm-zip": "^0.5.10", - "aws-cdk-lib": "^2.118.0" + "aws-cdk-lib": "^2.124.0" }, "devDependencies": { "@types/adm-zip": "^0.5.2", @@ -36,9 +36,9 @@ } }, "node_modules/@aws-cdk/asset-awscli-v1": { - "version": "2.2.201", - "resolved": "https://registry.npmjs.org/@aws-cdk/asset-awscli-v1/-/asset-awscli-v1-2.2.201.tgz", - "integrity": "sha512-INZqcwDinNaIdb5CtW3ez5s943nX5stGBQS6VOP2JDlOFP81hM3fds/9NDknipqfUkZM43dx+HgVvkXYXXARCQ==" + "version": "2.2.202", + "resolved": "https://registry.npmjs.org/@aws-cdk/asset-awscli-v1/-/asset-awscli-v1-2.2.202.tgz", + "integrity": "sha512-JqlF0D4+EVugnG5dAsNZMqhu3HW7ehOXm5SDMxMbXNDMdsF0pxtQKNHRl52z1U9igsHmaFpUgSGjbhAJ+0JONg==" }, "node_modules/@aws-cdk/asset-kubectl-v20": { "version": "2.1.2", @@ -1313,9 +1313,9 @@ } }, "node_modules/aws-cdk-lib": { - "version": "2.118.0", - "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.118.0.tgz", - "integrity": "sha512-i3At9HOuXNVLxQCo/y7Sb2Zj4Ir4tichG+755AAldebRcqXrCJizZq+sMMt6/Bkjggj2imWmhwHPZw518M9VMw==", + "version": "2.125.0", + "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.125.0.tgz", + "integrity": "sha512-yRcHuvpPYHuvffeJCnTSIqo6y+Qjeuf+BKmr/oyMcMhyfIzcGFFhh+ZQRCTYIJTfTyU6nh73TLhsZ4TmzFuBBA==", "bundleDependencies": [ "@balena/dockerignore", "case", @@ -1329,7 +1329,7 @@ "yaml" ], "dependencies": { - "@aws-cdk/asset-awscli-v1": "^2.2.201", + "@aws-cdk/asset-awscli-v1": "^2.2.202", "@aws-cdk/asset-kubectl-v20": "^2.1.2", "@aws-cdk/asset-node-proxy-agent-v6": "^2.0.1", "@balena/dockerignore": "^1.0.2", @@ -1776,12 +1776,12 @@ } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -2235,9 +2235,9 @@ } }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "dependencies": { "to-regex-range": "^5.0.1" diff --git a/deployment/cdk-solution-helper/package.json b/deployment/cdk-solution-helper/package.json index 5d5dd1173..31dac2f80 100644 --- a/deployment/cdk-solution-helper/package.json +++ b/deployment/cdk-solution-helper/package.json @@ -22,7 +22,7 @@ }, "dependencies": { "adm-zip": "^0.5.10", - "aws-cdk-lib": "^2.118.0" + "aws-cdk-lib": "^2.124.0" }, "overrides": { "semver": "7.5.4" diff --git a/source/constructs/bin/constructs.ts b/source/constructs/bin/constructs.ts index 0999d2005..67b0c2ad7 100644 --- a/source/constructs/bin/constructs.ts +++ b/source/constructs/bin/constructs.ts @@ -20,12 +20,13 @@ if (DIST_OUTPUT_BUCKET && SOLUTION_NAME && VERSION) const app = new App(); const solutionDisplayName = "Serverless Image Handler"; -const description = `(${app.node.tryGetContext("solutionId")}) - ${solutionDisplayName}. Version ${VERSION ?? app.node.tryGetContext("solutionVersion")}`; +const solutionVersion = VERSION ?? app.node.tryGetContext("solutionVersion"); +const description = `(${app.node.tryGetContext("solutionId")}) - ${solutionDisplayName}. Version ${solutionVersion}`; // eslint-disable-next-line no-new new ServerlessImageHandlerStack(app, "ServerlessImageHandlerStack", { - synthesizer: synthesizer, - description: description, + synthesizer, + description, solutionId: app.node.tryGetContext("solutionId"), - solutionVersion: app.node.tryGetContext("solutionVersion"), + solutionVersion, solutionName: app.node.tryGetContext("solutionName"), }); diff --git a/source/constructs/cdk.json b/source/constructs/cdk.json index e28db9f53..3b4d97733 100644 --- a/source/constructs/cdk.json +++ b/source/constructs/cdk.json @@ -2,7 +2,7 @@ "app": "npx ts-node --prefer-ts-exts bin/constructs.ts", "context": { "solutionId": "SO0023", - "solutionVersion": "custom-v6.2.5", + "solutionVersion": "custom-v6.2.6", "solutionName": "serverless-image-handler" } } \ No newline at end of file diff --git a/source/constructs/lib/back-end/back-end-construct.ts b/source/constructs/lib/back-end/back-end-construct.ts index d1ce111c6..b1d5d616e 100644 --- a/source/constructs/lib/back-end/back-end-construct.ts +++ b/source/constructs/lib/back-end/back-end-construct.ts @@ -31,11 +31,13 @@ import * as api from "aws-cdk-lib/aws-apigateway"; export interface BackEndProps extends SolutionConstructProps { readonly solutionVersion: string; + readonly solutionId: string; readonly solutionName: string; readonly secretsManagerPolicy: Policy; readonly logsBucket: IBucket; readonly uuid: string; readonly cloudFrontPriceClass: string; + readonly createSourceBucketsResource: (key?: string) => string[]; } export class BackEnd extends Construct { @@ -64,15 +66,16 @@ export class BackEnd extends Construct { ], }), new PolicyStatement({ - actions: ["s3:GetObject", "s3:PutObject", "s3:ListBucket"], - resources: [ - Stack.of(this).formatArn({ - service: "s3", - resource: "*", - region: "", - account: "", - }), - ], + actions: ["s3:GetObject"], + resources: props.createSourceBucketsResource("/*"), + }), + new PolicyStatement({ + actions: ["s3:ListBucket"], + resources: props.createSourceBucketsResource(), + }), + new PolicyStatement({ + actions: ["s3:GetObject"], + resources: [`arn:aws:s3:::${props.fallbackImageS3Bucket}/${props.fallbackImageS3KeyBucket}`], }), new PolicyStatement({ actions: ["rekognition:DetectFaces", "rekognition:DetectModerationLabels"], @@ -106,6 +109,8 @@ export class BackEnd extends Construct { ENABLE_DEFAULT_FALLBACK_IMAGE: props.enableDefaultFallbackImage, DEFAULT_FALLBACK_IMAGE_BUCKET: props.fallbackImageS3Bucket, DEFAULT_FALLBACK_IMAGE_KEY: props.fallbackImageS3KeyBucket, + SOLUTION_VERSION: props.solutionVersion, + SOLUTION_ID: props.solutionId, }, bundling: { externalModules: ["sharp"], diff --git a/source/constructs/lib/common-resources/custom-resources/custom-resource-construct.ts b/source/constructs/lib/common-resources/custom-resources/custom-resource-construct.ts index 14f1bd0e7..cbc43b91e 100644 --- a/source/constructs/lib/common-resources/custom-resources/custom-resource-construct.ts +++ b/source/constructs/lib/common-resources/custom-resources/custom-resource-construct.ts @@ -7,9 +7,9 @@ import { Function as LambdaFunction, Runtime } from "aws-cdk-lib/aws-lambda"; import { NodejsFunction } from "aws-cdk-lib/aws-lambda-nodejs"; import { Bucket, IBucket } from "aws-cdk-lib/aws-s3"; import { BucketDeployment, Source as S3Source } from "aws-cdk-lib/aws-s3-deployment"; -import { ArnFormat, Aspects, Aws, CfnCondition, CfnResource, CustomResource, Duration, Lazy, Stack } from "aws-cdk-lib"; +import { ArnFormat, Aspects, Aws, CfnCondition, CfnResource, CustomResource, Duration, Fn, Lazy, Stack } from "aws-cdk-lib"; import { Construct } from "constructs"; -import { addCfnSuppressRules } from "../../../utils/utils"; +import { addCfnCondition, addCfnSuppressRules } from "../../../utils/utils"; import { SolutionConstructProps } from "../../types"; import { CommonResourcesProps, Conditions } from "../common-resources-construct"; @@ -45,7 +45,6 @@ export interface SetupValidateSecretsManagerProps { } export class CustomResourcesConstruct extends Construct { - private readonly solutionVersion: string; private readonly conditions: Conditions; private readonly customResourceRole: Role; private readonly customResourceLambda: LambdaFunction; @@ -54,7 +53,6 @@ export class CustomResourcesConstruct extends Construct { constructor(scope: Construct, id: string, props: CustomResourcesConstructProps) { super(scope, id); - this.solutionVersion = props.solutionVersion; this.conditions = props.conditions; this.customResourceRole = new Role(this, "CustomResourceRole", { @@ -75,16 +73,26 @@ export class CustomResourcesConstruct extends Construct { }), ], }), + new PolicyStatement({ + actions: ['s3:ListBucket'], + resources: this.createSourceBucketsResource() + }), + new PolicyStatement({ + actions: [ + "s3:GetObject", + ], + resources: [ + `arn:aws:s3:::${props.fallbackImageS3Bucket}/${props.fallbackImageS3KeyBucket}`, + ], + }), new PolicyStatement({ actions: [ "s3:putBucketAcl", "s3:putEncryptionConfiguration", "s3:putBucketPolicy", "s3:CreateBucket", - "s3:GetObject", - "s3:PutObject", - "s3:ListBucket", "s3:PutBucketOwnershipControls", + "s3:PutBucketTagging" ], resources: [ Stack.of(this).formatArn({ @@ -142,6 +150,21 @@ export class CustomResourcesConstruct extends Construct { this.uuid = customResourceUuid.getAttString("UUID"); } + public setupWebsiteHostingBucketPolicy(websiteHostingBucket: IBucket) { + const websiteHostingBucketPolicy = new Policy(this, "WebsiteHostingBucketPolicy", { + document: new PolicyDocument({ + statements: [ + new PolicyStatement({ + actions: ["s3:GetObject", "s3:PutObject",], + resources: [websiteHostingBucket.bucketArn + "/*"], + }), + ], + }), + roles: [this.customResourceRole], + }) + addCfnCondition(websiteHostingBucketPolicy, this.conditions.deployUICondition); + }; + public setupAnonymousMetric(props: AnonymousMetricCustomResourceProps) { this.createCustomResource("CustomResourceAnonymousMetric", this.customResourceLambda, { CustomAction: "sendMetric", @@ -181,7 +204,9 @@ export class CustomResourcesConstruct extends Construct { // Stage static assets for the front-end from the local /* eslint-disable no-new */ const bucketDeployment = new BucketDeployment(this, "DeployWebsite", { - sources: [S3Source.asset(path.join(__dirname, "../../../../demo-ui"))], + sources: [ + S3Source.asset(path.join(__dirname, "../../../../demo-ui"), { exclude: ["node_modules/*"] }), + ], destinationBucket: props.hostingBucket, exclude: ["demo-ui-config.js"], }); @@ -235,6 +260,22 @@ export class CustomResourcesConstruct extends Construct { return optInRegionAccessLogBucket; } + public createSourceBucketsResource(resourceName: string = "") { + return Fn.split( + ',', + Fn.sub( + `arn:aws:s3:::\${rest}${resourceName}`, + + { + rest: Fn.join( + `${resourceName},arn:aws:s3:::`, + Fn.split(",", Fn.join("", Fn.split(" ", Fn.ref('SourceBucketsParameter')))) + ), + }, + ), + ) + } + private createCustomResource( id: string, customResourceFunction: LambdaFunction, diff --git a/source/constructs/lib/serverless-image-stack.ts b/source/constructs/lib/serverless-image-stack.ts index f92104947..ebf50161c 100644 --- a/source/constructs/lib/serverless-image-stack.ts +++ b/source/constructs/lib/serverless-image-stack.ts @@ -36,7 +36,7 @@ export class ServerlessImageHandlerStack extends Stack { const sourceBucketsParameter = new CfnParameter(this, "SourceBucketsParameter", { type: "String", description: - "(Required) List the buckets (comma-separated) within your account that contain original image files. If you plan to use Thumbor or Custom image requests with this solution, the source bucket for those requests will be the first bucket listed in this field.", + "(Required) List the buckets (comma-separated) within your account that contain original image files. If you plan to use Thumbor or Custom image requests with this solution, the source bucket for those requests will default to the first bucket listed in this field.", allowedPattern: ".+", default: "defaultBucket, bucketNo2, bucketNo3, ...", }); @@ -172,14 +172,18 @@ export class ServerlessImageHandlerStack extends Stack { const backEnd = new BackEnd(this, "BackEnd", { solutionVersion: props.solutionVersion, + solutionId: props.solutionId, solutionName: props.solutionName, secretsManagerPolicy: commonResources.secretsManagerPolicy, logsBucket: commonResources.logsBucket, uuid: commonResources.customResources.uuid, cloudFrontPriceClass: cloudFrontPriceClassParameter.valueAsString, + createSourceBucketsResource: commonResources.customResources.createSourceBucketsResource, ...solutionConstructProps, }); + commonResources.customResources.setupWebsiteHostingBucketPolicy(frontEnd.websiteHostingBucket); + commonResources.customResources.setupAnonymousMetric({ anonymousData: anonymousUsage, ...solutionConstructProps, @@ -319,6 +323,10 @@ export class ServerlessImageHandlerStack extends Stack { value: logRetentionPeriodParameter.valueAsString, description: "Number of days for event logs from Lambda to be retained in CloudWatch.", }); + new CfnOutput(this, "CloudFrontLoggingBucket", { + value: commonResources.logsBucket.bucketName, + description: "Amazon S3 bucket for storing CloudFront access logs.", + }) Aspects.of(this).add(new SuppressLambdaFunctionCfnRulesAspect()); Tags.of(this).add("SolutionId", props.solutionId); diff --git a/source/constructs/package-lock.json b/source/constructs/package-lock.json index 00f6fbfcd..921e951aa 100644 --- a/source/constructs/package-lock.json +++ b/source/constructs/package-lock.json @@ -1,12 +1,12 @@ { "name": "constructs", - "version": "6.2.5", + "version": "6.2.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "constructs", - "version": "6.2.5", + "version": "6.2.6", "license": "Apache-2.0", "dependencies": { "sharp": "^0.32.6" @@ -16,14 +16,14 @@ }, "devDependencies": { "@aws-cdk/aws-servicecatalogappregistry-alpha": "v2.118.0-alpha.0", - "@aws-solutions-constructs/aws-apigateway-lambda": "2.47.0", - "@aws-solutions-constructs/aws-cloudfront-apigateway-lambda": "2.47.0", - "@aws-solutions-constructs/aws-cloudfront-s3": "2.47.0", - "@aws-solutions-constructs/core": "2.47.0", + "@aws-solutions-constructs/aws-apigateway-lambda": "2.51.0", + "@aws-solutions-constructs/aws-cloudfront-apigateway-lambda": "2.51.0", + "@aws-solutions-constructs/aws-cloudfront-s3": "2.51.0", + "@aws-solutions-constructs/core": "2.51.0", "@types/jest": "^29.5.6", "@types/node": "^20.10.4", - "aws-cdk": "^2.118.0", - "aws-cdk-lib": "^2.118.0", + "aws-cdk": "^2.124.0", + "aws-cdk-lib": "^2.124.0", "constructs": "^10.3.0", "esbuild": "^0.19.10", "jest": "^29.7.0", @@ -46,9 +46,9 @@ } }, "node_modules/@aws-cdk/asset-awscli-v1": { - "version": "2.2.201", - "resolved": "https://registry.npmjs.org/@aws-cdk/asset-awscli-v1/-/asset-awscli-v1-2.2.201.tgz", - "integrity": "sha512-INZqcwDinNaIdb5CtW3ez5s943nX5stGBQS6VOP2JDlOFP81hM3fds/9NDknipqfUkZM43dx+HgVvkXYXXARCQ==", + "version": "2.2.202", + "resolved": "https://registry.npmjs.org/@aws-cdk/asset-awscli-v1/-/asset-awscli-v1-2.2.202.tgz", + "integrity": "sha512-JqlF0D4+EVugnG5dAsNZMqhu3HW7ehOXm5SDMxMbXNDMdsF0pxtQKNHRl52z1U9igsHmaFpUgSGjbhAJ+0JONg==", "dev": true }, "node_modules/@aws-cdk/asset-kubectl-v20": { @@ -77,67 +77,73 @@ } }, "node_modules/@aws-solutions-constructs/aws-apigateway-lambda": { - "version": "2.47.0", - "resolved": "https://registry.npmjs.org/@aws-solutions-constructs/aws-apigateway-lambda/-/aws-apigateway-lambda-2.47.0.tgz", - "integrity": "sha512-MqK7UlMxptymPX5MUI2q6w4xHIrLqq5xaZYHTCDOTcPxUKgzp5gOAKruga27x24MIIBWRojtfp0wE6Tc6YWv4g==", + "version": "2.51.0", + "resolved": "https://registry.npmjs.org/@aws-solutions-constructs/aws-apigateway-lambda/-/aws-apigateway-lambda-2.51.0.tgz", + "integrity": "sha512-7RWaXGa//9j3YVB8tZxfsy8S0c9EokdMv+JdSNwBzygPuaZBnR8eDnjiPTor8MHv9fhhtSaLolMDpqdYpzFo7g==", "dev": true, "dependencies": { - "@aws-solutions-constructs/core": "2.47.0" + "@aws-solutions-constructs/core": "2.51.0", + "constructs": "^10.0.0" }, "peerDependencies": { - "@aws-solutions-constructs/core": "2.47.0", - "aws-cdk-lib": "^2.111.0", + "@aws-solutions-constructs/core": "2.51.0", + "aws-cdk-lib": "^2.118.0", "constructs": "^10.0.0" } }, "node_modules/@aws-solutions-constructs/aws-cloudfront-apigateway": { - "version": "2.47.0", - "resolved": "https://registry.npmjs.org/@aws-solutions-constructs/aws-cloudfront-apigateway/-/aws-cloudfront-apigateway-2.47.0.tgz", - "integrity": "sha512-xOQJG4lg6CzTMe2Bhk95x4L3esNVsel1e1HYbkGwYmIVYq47owSp93z3FX+Cliz4ZpgTZE2K1XZEurgAFSbnbg==", + "version": "2.51.0", + "resolved": "https://registry.npmjs.org/@aws-solutions-constructs/aws-cloudfront-apigateway/-/aws-cloudfront-apigateway-2.51.0.tgz", + "integrity": "sha512-3UTEc+76cNOe7qn3qAwhNHlq7JIfwe07nijS9cfkUThAsfi4faP49+rcAzluZPx7dPGFUp0zK4MYiiXaVuN8vQ==", "dev": true, "dependencies": { - "@aws-solutions-constructs/core": "2.47.0" + "@aws-solutions-constructs/core": "2.51.0", + "constructs": "^10.0.0" }, "peerDependencies": { - "@aws-solutions-constructs/core": "2.47.0", - "aws-cdk-lib": "^2.111.0", + "@aws-solutions-constructs/core": "2.51.0", + "aws-cdk-lib": "^2.118.0", "constructs": "^10.0.0" } }, "node_modules/@aws-solutions-constructs/aws-cloudfront-apigateway-lambda": { - "version": "2.47.0", - "resolved": "https://registry.npmjs.org/@aws-solutions-constructs/aws-cloudfront-apigateway-lambda/-/aws-cloudfront-apigateway-lambda-2.47.0.tgz", - "integrity": "sha512-spTY2A8g3jvxTDYwyCVSoubdZoGsgVlgfiK+J7UuUNtUMNA9ss6egmSJjf2rR15UYyRGhbnDkJ+7OJOrfyfMYA==", + "version": "2.51.0", + "resolved": "https://registry.npmjs.org/@aws-solutions-constructs/aws-cloudfront-apigateway-lambda/-/aws-cloudfront-apigateway-lambda-2.51.0.tgz", + "integrity": "sha512-juVPg0h9hP1i2YKh+avyBCpud9pOPPzZHW9+hcf6LlqdcYp0VrrmNFta3W7GhFFmXLxU77Nwn80i56Wn2chzxg==", "dev": true, "dependencies": { - "@aws-solutions-constructs/aws-cloudfront-apigateway": "2.47.0", - "@aws-solutions-constructs/core": "2.47.0" + "@aws-solutions-constructs/aws-cloudfront-apigateway": "2.51.0", + "@aws-solutions-constructs/core": "2.51.0", + "constructs": "^10.0.0" }, "peerDependencies": { - "@aws-solutions-constructs/aws-cloudfront-apigateway": "2.47.0", - "@aws-solutions-constructs/core": "2.47.0", - "aws-cdk-lib": "^2.111.0", + "@aws-solutions-constructs/aws-cloudfront-apigateway": "2.51.0", + "@aws-solutions-constructs/core": "2.51.0", + "aws-cdk-lib": "^2.118.0", "constructs": "^10.0.0" } }, "node_modules/@aws-solutions-constructs/aws-cloudfront-s3": { - "version": "2.47.0", - "resolved": "https://registry.npmjs.org/@aws-solutions-constructs/aws-cloudfront-s3/-/aws-cloudfront-s3-2.47.0.tgz", - "integrity": "sha512-5p0cF0bwt8gN83mLAcH2sP4PvPMFaXA1dVttlJ+uMMOeDQD7N/CTfXrHU4UaU6zIv4B/ESiwYr0kaLFq5L3Qqw==", + "version": "2.51.0", + "resolved": "https://registry.npmjs.org/@aws-solutions-constructs/aws-cloudfront-s3/-/aws-cloudfront-s3-2.51.0.tgz", + "integrity": "sha512-11TV5xdrT48lQams3fR8b7GYcECqrQ6lbvCfyENqbmozWwVrnmNZxoFcIy4VF9oQPwx07kKsAsgsHnuDJhQjBQ==", "dev": true, "dependencies": { - "@aws-solutions-constructs/core": "2.47.0" + "@aws-solutions-constructs/core": "2.51.0", + "@aws-solutions-constructs/resources": "2.51.0", + "constructs": "^10.0.0" }, "peerDependencies": { - "@aws-solutions-constructs/core": "2.47.0", - "aws-cdk-lib": "^2.111.0", + "@aws-solutions-constructs/core": "2.51.0", + "@aws-solutions-constructs/resources": "2.51.0", + "aws-cdk-lib": "^2.118.0", "constructs": "^10.0.0" } }, "node_modules/@aws-solutions-constructs/core": { - "version": "2.47.0", - "resolved": "https://registry.npmjs.org/@aws-solutions-constructs/core/-/core-2.47.0.tgz", - "integrity": "sha512-k7KOnAYlFoEjxeU5Z5ijRkmc8/CVjaza3x934HWK+9K3tzVRicNwC1zlHNrH95Oq86ICjYgSH2AQEYgt2r8lCQ==", + "version": "2.51.0", + "resolved": "https://registry.npmjs.org/@aws-solutions-constructs/core/-/core-2.51.0.tgz", + "integrity": "sha512-55LQfMgxbXSpfLduOygTNUrAOxlj09lDufbL+mcncB/1hDkJiawYQYx/OUcFWFfGSheXRwNoxOP4FYVS/7cCDQ==", "bundleDependencies": [ "deepmerge", "npmlog", @@ -145,12 +151,13 @@ ], "dev": true, "dependencies": { + "constructs": "^10.0.0", "deep-diff": "^1.0.2", "deepmerge": "^4.0.0", "npmlog": "^4.1.2" }, "peerDependencies": { - "aws-cdk-lib": "^2.111.0", + "aws-cdk-lib": "^2.118.0", "constructs": "^10.0.0" } }, @@ -436,6 +443,29 @@ "node": ">=8" } }, + "node_modules/@aws-solutions-constructs/resources": { + "version": "2.51.0", + "resolved": "https://registry.npmjs.org/@aws-solutions-constructs/resources/-/resources-2.51.0.tgz", + "integrity": "sha512-QYZjnyGnMsdKYxYPfQ6vVn2AfbnuwuzVBbWiC/mJhWBE6N5IPXd7YRj+dWT0D440fdFx/sBUpsWWPWeWbgwNWw==", + "bundleDependencies": [ + "@aws-sdk/client-kms", + "@aws-sdk/client-s3", + "aws-sdk-client-mock" + ], + "dev": true, + "dependencies": { + "@aws-sdk/client-kms": "^3.478.0", + "@aws-sdk/client-s3": "^3.478.0", + "@aws-solutions-constructs/core": "2.51.0", + "aws-sdk-client-mock": "^3.0.0", + "constructs": "^10.0.0" + }, + "peerDependencies": { + "@aws-solutions-constructs/core": "2.51.0", + "aws-cdk-lib": "^2.118.0", + "constructs": "^10.0.0" + } + }, "node_modules/@babel/code-frame": { "version": "7.22.13", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", @@ -2050,9 +2080,9 @@ } }, "node_modules/aws-cdk": { - "version": "2.118.0", - "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.118.0.tgz", - "integrity": "sha512-va4F7fyj+l9oNV39supHeGr+oHBrVds6+3mruLxGmCRnGf3nKfPB8Jy/jd6TnljY8Y6yPZ6bmYFS3CiUZbOATA==", + "version": "2.124.0", + "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.124.0.tgz", + "integrity": "sha512-kUOfqwIAaTEx4ZozojZEhWa8G+O9KU+P0tERtDVmTw9ip4QXNMwTTkjj/IPtoH8qfXGdeibTQ9MJwRvHOR8kXQ==", "dev": true, "bin": { "cdk": "bin/cdk" @@ -2065,9 +2095,9 @@ } }, "node_modules/aws-cdk-lib": { - "version": "2.118.0", - "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.118.0.tgz", - "integrity": "sha512-i3At9HOuXNVLxQCo/y7Sb2Zj4Ir4tichG+755AAldebRcqXrCJizZq+sMMt6/Bkjggj2imWmhwHPZw518M9VMw==", + "version": "2.124.0", + "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.124.0.tgz", + "integrity": "sha512-K/Tey8TMw30GO6UD0qb19CPhBMZhleGshz520ZnbDUJwNfFtejwZOnpmRMOdUP9f4tHc5BrXl1VGsZtXtUaGhg==", "bundleDependencies": [ "@balena/dockerignore", "case", @@ -2082,7 +2112,7 @@ ], "dev": true, "dependencies": { - "@aws-cdk/asset-awscli-v1": "^2.2.201", + "@aws-cdk/asset-awscli-v1": "^2.2.202", "@aws-cdk/asset-kubectl-v20": "^2.1.2", "@aws-cdk/asset-node-proxy-agent-v6": "^2.0.1", "@balena/dockerignore": "^1.0.2", @@ -2597,12 +2627,12 @@ } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -3192,9 +3222,9 @@ } }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "dependencies": { "to-regex-range": "^5.0.1" diff --git a/source/constructs/package.json b/source/constructs/package.json index d08e491a6..ee8fb6e22 100644 --- a/source/constructs/package.json +++ b/source/constructs/package.json @@ -1,6 +1,6 @@ { "name": "constructs", - "version": "6.2.5", + "version": "6.2.6", "description": "Serverless Image Handler Constructs", "license": "Apache-2.0", "author": { @@ -17,18 +17,19 @@ "pretest": "npm run clean:install", "build": "tsc", "watch": "tsc -w", - "test": "overrideWarningsEnabled=false jest --coverage" + "test": "overrideWarningsEnabled=false jest --coverage", + "bump-version": "npm version $(cat ../../VERSION.txt) --allow-same-version" }, "devDependencies": { "@aws-cdk/aws-servicecatalogappregistry-alpha": "v2.118.0-alpha.0", - "@aws-solutions-constructs/aws-apigateway-lambda": "2.47.0", - "@aws-solutions-constructs/aws-cloudfront-apigateway-lambda": "2.47.0", - "@aws-solutions-constructs/aws-cloudfront-s3": "2.47.0", - "@aws-solutions-constructs/core": "2.47.0", + "@aws-solutions-constructs/aws-apigateway-lambda": "2.51.0", + "@aws-solutions-constructs/aws-cloudfront-apigateway-lambda": "2.51.0", + "@aws-solutions-constructs/aws-cloudfront-s3": "2.51.0", + "@aws-solutions-constructs/core": "2.51.0", "@types/jest": "^29.5.6", "@types/node": "^20.10.4", - "aws-cdk": "^2.118.0", - "aws-cdk-lib": "^2.118.0", + "aws-cdk": "^2.124.0", + "aws-cdk-lib": "^2.124.0", "constructs": "^10.3.0", "esbuild": "^0.19.10", "jest": "^29.7.0", diff --git a/source/constructs/test/__snapshots__/constructs.test.ts.snap b/source/constructs/test/__snapshots__/constructs.test.ts.snap index e1c211371..d872ccdd5 100644 --- a/source/constructs/test/__snapshots__/constructs.test.ts.snap +++ b/source/constructs/test/__snapshots__/constructs.test.ts.snap @@ -162,6 +162,15 @@ exports[`Serverless Image Handler Stack Snapshot 1`] = ` ], }, }, + "CloudFrontLoggingBucket": { + "Description": "Amazon S3 bucket for storing CloudFront access logs.", + "Value": { + "Fn::GetAtt": [ + "CommonResourcesCustomResourcesLogBucketCustomResource2445A3AB", + "BucketName", + ], + }, + }, "CorsEnabled": { "Description": "Indicates whether Cross-Origin Resource Sharing (CORS) has been enabled for the image handler API.", "Value": { @@ -320,7 +329,7 @@ exports[`Serverless Image Handler Stack Snapshot 1`] = ` "SourceBucketsParameter": { "AllowedPattern": ".+", "Default": "defaultBucket, bucketNo2, bucketNo3, ...", - "Description": "(Required) List the buckets (comma-separated) within your account that contain original image files. If you plan to use Thumbor or Custom image requests with this solution, the source bucket for those requests will be the first bucket listed in this field.", + "Description": "(Required) List the buckets (comma-separated) within your account that contain original image files. If you plan to use Thumbor or Custom image requests with this solution, the source bucket for those requests will default to the first bucket listed in this field.", "Type": "String", }, }, @@ -350,7 +359,7 @@ exports[`Serverless Image Handler Stack Snapshot 1`] = ` "Solutions:ApplicationType": "AWS-Solutions", "Solutions:SolutionID": "S0ABC", "Solutions:SolutionName": "sih", - "Solutions:SolutionVersion": "v6.2.5", + "Solutions:SolutionVersion": "v6.2.6", }, }, "Type": "AWS::ServiceCatalogAppRegistry::Application", @@ -1050,21 +1059,98 @@ exports[`Serverless Image Handler Stack Snapshot 1`] = ` }, }, { - "Action": [ - "s3:GetObject", - "s3:PutObject", - "s3:ListBucket", - ], + "Action": "s3:GetObject", + "Effect": "Allow", + "Resource": { + "Fn::Split": [ + ",", + { + "Fn::Sub": [ + "arn:aws:s3:::\${rest}/*", + { + "rest": { + "Fn::Join": [ + "/*,arn:aws:s3:::", + { + "Fn::Split": [ + ",", + { + "Fn::Join": [ + "", + { + "Fn::Split": [ + " ", + { + "Ref": "SourceBucketsParameter", + }, + ], + }, + ], + }, + ], + }, + ], + }, + }, + ], + }, + ], + }, + }, + { + "Action": "s3:ListBucket", + "Effect": "Allow", + "Resource": { + "Fn::Split": [ + ",", + { + "Fn::Sub": [ + "arn:aws:s3:::\${rest}", + { + "rest": { + "Fn::Join": [ + ",arn:aws:s3:::", + { + "Fn::Split": [ + ",", + { + "Fn::Join": [ + "", + { + "Fn::Split": [ + " ", + { + "Ref": "SourceBucketsParameter", + }, + ], + }, + ], + }, + ], + }, + ], + }, + }, + ], + }, + ], + }, + }, + { + "Action": "s3:GetObject", "Effect": "Allow", "Resource": { "Fn::Join": [ "", [ - "arn:", + "arn:aws:s3:::", { - "Ref": "AWS::Partition", + "Ref": "FallbackImageS3BucketParameter", + }, + "/", + { + "Ref": "FallbackImageS3KeyParameter", }, - ":s3:::*", ], ], }, @@ -1142,7 +1228,7 @@ exports[`Serverless Image Handler Stack Snapshot 1`] = ` }, "S3Key": "Omitted to remove snapshot dependency on hash", }, - "Description": "sih (v6.2.5): Performs image edits and manipulations", + "Description": "sih (v6.2.6): Performs image edits and manipulations", "Environment": { "Variables": { "AUTO_WEBP": { @@ -1175,6 +1261,8 @@ exports[`Serverless Image Handler Stack Snapshot 1`] = ` "SECRET_KEY": { "Ref": "SecretsManagerKeyParameter", }, + "SOLUTION_ID": "S0ABC", + "SOLUTION_VERSION": "v6.2.6", "SOURCE_BUCKETS": { "Ref": "SourceBucketsParameter", }, @@ -1409,13 +1497,13 @@ exports[`Serverless Image Handler Stack Snapshot 1`] = ` }, "S3Key": "Omitted to remove snapshot dependency on hash", }, - "Description": "sih (v6.2.5): Custom resource", + "Description": "sih (v6.2.6): Custom resource", "Environment": { "Variables": { "AWS_NODEJS_CONNECTION_REUSE_ENABLED": "1", "RETRY_SECONDS": "5", "SOLUTION_ID": "S0ABC", - "SOLUTION_VERSION": "v6.2.5", + "SOLUTION_VERSION": "v6.2.6", }, }, "Handler": "index.handler", @@ -1494,16 +1582,72 @@ exports[`Serverless Image Handler Stack Snapshot 1`] = ` ], }, }, + { + "Action": "s3:ListBucket", + "Effect": "Allow", + "Resource": { + "Fn::Split": [ + ",", + { + "Fn::Sub": [ + "arn:aws:s3:::\${rest}", + { + "rest": { + "Fn::Join": [ + ",arn:aws:s3:::", + { + "Fn::Split": [ + ",", + { + "Fn::Join": [ + "", + { + "Fn::Split": [ + " ", + { + "Ref": "SourceBucketsParameter", + }, + ], + }, + ], + }, + ], + }, + ], + }, + }, + ], + }, + ], + }, + }, + { + "Action": "s3:GetObject", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:aws:s3:::", + { + "Ref": "FallbackImageS3BucketParameter", + }, + "/", + { + "Ref": "FallbackImageS3KeyParameter", + }, + ], + ], + }, + }, { "Action": [ "s3:putBucketAcl", "s3:putEncryptionConfiguration", "s3:putBucketPolicy", "s3:CreateBucket", - "s3:GetObject", - "s3:PutObject", - "s3:ListBucket", "s3:PutBucketOwnershipControls", + "s3:PutBucketTagging", ], "Effect": "Allow", "Resource": { @@ -1600,7 +1744,7 @@ exports[`Serverless Image Handler Stack Snapshot 1`] = ` }, ], "SourceObjectKeys": [ - "c301c2cce52bb1b36720a115d657907f3ec9fa20bd385b4d81bf451fbe77fe4b.zip", + "Omitted to remove snapshot dependency on demo ui module hash", ], }, "Type": "Custom::CDKBucketDeployment", @@ -1676,6 +1820,44 @@ exports[`Serverless Image Handler Stack Snapshot 1`] = ` "Type": "AWS::CloudFormation::CustomResource", "UpdateReplacePolicy": "Delete", }, + "CommonResourcesCustomResourcesWebsiteHostingBucketPolicy3C526944": { + "Condition": "CommonResourcesDeployDemoUICondition308D3B09", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "s3:GetObject", + "s3:PutObject", + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "FrontEndDistributionToS3S3Bucket3A171D78", + "Arn", + ], + }, + "/*", + ], + ], + }, + }, + ], + "Version": "2012-10-17", + }, + "PolicyName": "CommonResourcesCustomResourcesWebsiteHostingBucketPolicy3C526944", + "Roles": [ + { + "Ref": "CommonResourcesCustomResourcesCustomResourceRole8958A1ED", + }, + ], + }, + "Type": "AWS::IAM::Policy", + }, "CommonResourcesSecretsManagerPolicy45FE005E": { "Condition": "CommonResourcesEnableSignatureCondition909DC7A1", "Properties": { @@ -1922,7 +2104,7 @@ exports[`Serverless Image Handler Stack Snapshot 1`] = ` "applicationType": "AWS-Solutions", "solutionID": "S0ABC", "solutionName": "sih", - "version": "v6.2.5", + "version": "v6.2.6", }, "Description": "Attribute group for solution information", "Name": { @@ -2032,18 +2214,14 @@ exports[`Serverless Image Handler Stack Snapshot 1`] = ` ], }, "Id": "TestStackFrontEndDistributionToS3CloudFrontDistributionOrigin12FCDC222", + "OriginAccessControlId": { + "Fn::GetAtt": [ + "FrontEndDistributionToS3CloudFrontOac2BE9C90D", + "Id", + ], + }, "S3OriginConfig": { - "OriginAccessIdentity": { - "Fn::Join": [ - "", - [ - "origin-access-identity/cloudfront/", - { - "Ref": "FrontEndDistributionToS3CloudFrontDistributionOrigin1S3OriginD10E575E", - }, - ], - ], - }, + "OriginAccessIdentity": "", }, }, ], @@ -2057,14 +2235,38 @@ exports[`Serverless Image Handler Stack Snapshot 1`] = ` }, "Type": "AWS::CloudFront::Distribution", }, - "FrontEndDistributionToS3CloudFrontDistributionOrigin1S3OriginD10E575E": { + "FrontEndDistributionToS3CloudFrontOac2BE9C90D": { "Condition": "CommonResourcesDeployDemoUICondition308D3B09", "Properties": { - "CloudFrontOriginAccessIdentityConfig": { - "Comment": "Identity for TestStackFrontEndDistributionToS3CloudFrontDistributionOrigin12FCDC222", + "OriginAccessControlConfig": { + "Description": "Origin access control provisioned by aws-cloudfront-s3", + "Name": { + "Fn::Join": [ + "", + [ + "aws-cloudfront-s3-DistnToS3-", + { + "Fn::Select": [ + 2, + { + "Fn::Split": [ + "/", + { + "Ref": "AWS::StackId", + }, + ], + }, + ], + }, + ], + ], + }, + "OriginAccessControlOriginType": "s3", + "SigningBehavior": "always", + "SigningProtocol": "sigv4", }, }, - "Type": "AWS::CloudFront::CloudFrontOriginAccessIdentity", + "Type": "AWS::CloudFront::OriginAccessControl", }, "FrontEndDistributionToS3S3Bucket3A171D78": { "Condition": "CommonResourcesDeployDemoUICondition308D3B09", @@ -2179,14 +2381,28 @@ exports[`Serverless Image Handler Stack Snapshot 1`] = ` }, { "Action": "s3:GetObject", + "Condition": { + "StringEquals": { + "AWS:SourceArn": { + "Fn::Join": [ + "", + [ + "arn:aws:cloudfront::", + { + "Ref": "AWS::AccountId", + }, + ":distribution/", + { + "Ref": "FrontEndDistributionToS3CloudFrontDistribution15FE13D0", + }, + ], + ], + }, + }, + }, "Effect": "Allow", "Principal": { - "CanonicalUser": { - "Fn::GetAtt": [ - "FrontEndDistributionToS3CloudFrontDistributionOrigin1S3OriginD10E575E", - "S3CanonicalUserId", - ], - }, + "Service": "cloudfront.amazonaws.com", }, "Resource": { "Fn::Join": [ diff --git a/source/constructs/test/constructs.test.ts b/source/constructs/test/constructs.test.ts index b47f1f30a..3609dde8d 100644 --- a/source/constructs/test/constructs.test.ts +++ b/source/constructs/test/constructs.test.ts @@ -12,7 +12,7 @@ test("Serverless Image Handler Stack Snapshot", () => { const stack = new ServerlessImageHandlerStack(app, "TestStack", { solutionId: "S0ABC", solutionName: "sih", - solutionVersion: "v6.2.5", + solutionVersion: "v6.2.6", }); const template = Template.fromStack(stack); @@ -30,6 +30,11 @@ test("Serverless Image Handler Stack Snapshot", () => { if (templateJson.Resources[key].Properties?.Content?.S3Key) { templateJson.Resources[key].Properties.Content.S3Key = "Omitted to remove snapshot dependency on hash"; } + if (templateJson.Resources[key].Properties?.SourceObjectKeys) { + templateJson.Resources[key].Properties.SourceObjectKeys = [ + "Omitted to remove snapshot dependency on demo ui module hash", + ]; + } }); expect.assertions(1); diff --git a/source/custom-resource/index.ts b/source/custom-resource/index.ts index 336226f3b..62f3923aa 100644 --- a/source/custom-resource/index.ts +++ b/source/custom-resource/index.ts @@ -16,7 +16,6 @@ import { CheckSecretManagerRequestProperties, CheckSourceBucketsRequestProperties, CompletionStatus, - CopyS3AssetsRequestProperties, CreateLoggingBucketRequestProperties, CustomResourceActions, CustomResourceError, @@ -75,17 +74,6 @@ export async function handler(event: CustomResourceRequest, context: LambdaConte ); break; } - case CustomResourceActions.COPY_S3_ASSETS: { - const allowedRequestTypes = [CustomResourceRequestTypes.CREATE, CustomResourceRequestTypes.UPDATE]; - await performRequest( - copyS3Assets, - RequestType, - allowedRequestTypes, - response, - ResourceProperties as CopyS3AssetsRequestProperties - ); - break; - } case CustomResourceActions.CREATE_UUID: { const allowedRequestTypes = [CustomResourceRequestTypes.CREATE]; await performRequest(generateUUID, RequestType, allowedRequestTypes, response); @@ -131,7 +119,7 @@ export async function handler(event: CustomResourceRequest, context: LambdaConte RequestType, allowedRequestTypes, response, - ResourceProperties as CreateLoggingBucketRequestProperties + { ...ResourceProperties, StackId: event.StackId } as CreateLoggingBucketRequestProperties ); break; } @@ -369,75 +357,6 @@ async function putConfigFile( }; } -/** - * Copies assets from the source S3 bucket to the destination S3 bucket. - * @param requestProperties The request properties. - * @returns The result of copying assets. - */ -async function copyS3Assets( - requestProperties: CopyS3AssetsRequestProperties -): Promise<{ Message: string; Manifest: { Files: string[] } }> { - const { ManifestKey, SourceS3Bucket, SourceS3key, DestS3Bucket } = requestProperties; - - console.info(`Source bucket: ${SourceS3Bucket}`); - console.info(`Source prefix: ${SourceS3key}`); - console.info(`Destination bucket: ${DestS3Bucket}`); - - let manifest: { files: string[] }; - - // Download manifest - for (let retry = 1; retry <= RETRY_COUNT; retry++) { - try { - const getParams = { - Bucket: SourceS3Bucket, - Key: ManifestKey, - }; - const response = await s3Client.getObject(getParams).promise(); - manifest = JSON.parse(response.Body.toString()); - - break; - } catch (error) { - if (retry === RETRY_COUNT || error.code !== ErrorCodes.ACCESS_DENIED) { - console.error("Error occurred while getting manifest file."); - console.error(error); - - throw new CustomResourceError("GetManifestFailure", "Copy of website assets failed."); - } else { - console.info("Waiting for retry..."); - - await sleep(getRetryTimeout(retry)); - } - } - } - - // Copy asset files - try { - await Promise.all( - manifest.files.map(async (fileName: string) => { - const copyObjectParams = { - Bucket: DestS3Bucket, - CopySource: `${SourceS3Bucket}/${SourceS3key}/${fileName}`, - Key: fileName, - ContentType: getContentType(fileName), - }; - - console.debug(`Copying ${fileName} to ${DestS3Bucket}`); - return s3Client.copyObject(copyObjectParams).promise(); - }) - ); - - return { - Message: "Copy assets completed.", - Manifest: { Files: manifest.files }, - }; - } catch (error) { - console.error("Error occurred while copying assets."); - console.error(error); - - throw new CustomResourceError("CopyAssetsFailure", "Copy of website assets failed."); - } -} - /** * Generates UUID. * @returns Generated UUID. @@ -673,7 +592,7 @@ async function createCloudFrontLoggingBucket(requestProperties: CreateLoggingBuc await s3Client.putBucketPolicy(putBucketPolicyRequestParams).promise(); - console.info(`Successfully added policy added to bucket '${bucketName}'`); + console.info(`Successfully added policy to bucket '${bucketName}'`); } catch (error) { console.error(`Failed to add policy to bucket '${bucketName}'`); console.error(error); @@ -681,6 +600,29 @@ async function createCloudFrontLoggingBucket(requestProperties: CreateLoggingBuc throw error; } + // Add Stack tag + try { + console.info("Adding tag..."); + + const taggingParams = { + Bucket: bucketName, + Tagging: { + TagSet: [ + { + Key: "stack-id", + Value: requestProperties.StackId + }] + } + }; + await s3Client.putBucketTagging(taggingParams).promise(); + + console.info(`Successfully added tag to bucket '${bucketName}'`); + } catch (error) { + console.error(`Failed to add tag to bucket '${bucketName}'`); + console.error(error); + // Continue, failure here shouldn't block + } + return { BucketName: bucketName, Region: targetRegion }; } diff --git a/source/custom-resource/lib/enums.ts b/source/custom-resource/lib/enums.ts index 26731399a..c709dadec 100644 --- a/source/custom-resource/lib/enums.ts +++ b/source/custom-resource/lib/enums.ts @@ -4,7 +4,6 @@ export enum CustomResourceActions { SEND_ANONYMOUS_METRIC = "sendMetric", PUT_CONFIG_FILE = "putConfigFile", - COPY_S3_ASSETS = "copyS3assets", CREATE_UUID = "createUuid", CHECK_SOURCE_BUCKETS = "checkSourceBuckets", CHECK_SECRETS_MANAGER = "checkSecretsManager", diff --git a/source/custom-resource/lib/interfaces.ts b/source/custom-resource/lib/interfaces.ts index 2faf9fb12..f909f1a75 100644 --- a/source/custom-resource/lib/interfaces.ts +++ b/source/custom-resource/lib/interfaces.ts @@ -26,13 +26,6 @@ export interface PutConfigRequestProperties extends CustomResourceRequestPropert DestS3key: string; } -export interface CopyS3AssetsRequestProperties extends CustomResourceRequestPropertiesBase { - ManifestKey: string; - SourceS3Bucket: string; - SourceS3key: string; - DestS3Bucket: string; -} - export interface CheckSourceBucketsRequestProperties extends CustomResourceRequestPropertiesBase { SourceBuckets: string; } @@ -58,6 +51,7 @@ export interface PolicyStatement { export interface CreateLoggingBucketRequestProperties extends CustomResourceRequestPropertiesBase { BucketSuffix: string; + StackId: string; } export interface CustomResourceRequest { diff --git a/source/custom-resource/lib/types.ts b/source/custom-resource/lib/types.ts index ca6c7a5c1..b58ef2534 100644 --- a/source/custom-resource/lib/types.ts +++ b/source/custom-resource/lib/types.ts @@ -5,7 +5,6 @@ import { CheckFallbackImageRequestProperties, CheckSecretManagerRequestProperties, CheckSourceBucketsRequestProperties, - CopyS3AssetsRequestProperties, CreateLoggingBucketRequestProperties, CustomResourceRequestPropertiesBase, PutConfigRequestProperties, @@ -16,7 +15,6 @@ export type ResourcePropertyTypes = | CustomResourceRequestPropertiesBase | SendMetricsRequestProperties | PutConfigRequestProperties - | CopyS3AssetsRequestProperties | CheckSourceBucketsRequestProperties | CheckSecretManagerRequestProperties | CheckFallbackImageRequestProperties diff --git a/source/custom-resource/package-lock.json b/source/custom-resource/package-lock.json index fed789cfd..9bf970bb1 100644 --- a/source/custom-resource/package-lock.json +++ b/source/custom-resource/package-lock.json @@ -1,12 +1,12 @@ { "name": "custom-resource", - "version": "6.2.5", + "version": "6.2.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "custom-resource", - "version": "6.2.5", + "version": "6.2.6", "license": "Apache-2.0", "dependencies": { "aws-sdk": "^2.1529.0", @@ -1485,12 +1485,12 @@ } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -1984,9 +1984,9 @@ } }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "dependencies": { "to-regex-range": "^5.0.1" @@ -2009,9 +2009,9 @@ } }, "node_modules/follow-redirects": { - "version": "1.15.4", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.4.tgz", - "integrity": "sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==", + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", "funding": [ { "type": "individual", diff --git a/source/custom-resource/package.json b/source/custom-resource/package.json index d81cd5f6a..a2e85d717 100644 --- a/source/custom-resource/package.json +++ b/source/custom-resource/package.json @@ -1,6 +1,6 @@ { "name": "custom-resource", - "version": "6.2.5", + "version": "6.2.6", "private": true, "description": "Serverless Image Handler custom resource", "license": "Apache-2.0", @@ -12,7 +12,8 @@ "scripts": { "clean": "rm -rf node_modules/ dist/ coverage/", "pretest": "npm run clean && npm ci", - "test": "jest --coverage --silent" + "test": "jest --coverage --silent", + "bump-version": "npm version $(cat ../../VERSION.txt) --allow-same-version" }, "dependencies": { "aws-sdk": "^2.1529.0", diff --git a/source/custom-resource/test/create-logging-bucket.spec.ts b/source/custom-resource/test/create-logging-bucket.spec.ts index f817e2680..2a70d3789 100644 --- a/source/custom-resource/test/create-logging-bucket.spec.ts +++ b/source/custom-resource/test/create-logging-bucket.spec.ts @@ -22,6 +22,11 @@ describe("CREATE_LOGGING_BUCKET", () => { }, }; + beforeEach(() => { + consoleInfoSpy.mockReset() + consoleErrorSpy.mockReset() + }); + it("Should return success and bucket name", async () => { mockAwsEc2.describeRegions.mockImplementationOnce(() => ({ promise() { @@ -43,10 +48,15 @@ describe("CREATE_LOGGING_BUCKET", () => { return Promise.resolve(); }, })); + mockAwsS3.putBucketTagging.mockImplementation(() => ({ + promise() { + return Promise.resolve(); + }, + })); await handler(event, mockContext); - expect.assertions(4); + expect.assertions(5); expect(consoleInfoSpy).toHaveBeenCalledWith( expect.stringContaining("The opt-in status of the 'mock-region-1' region is 'opted-in'") @@ -60,7 +70,10 @@ describe("CREATE_LOGGING_BUCKET", () => { expect.stringMatching(/^Successfully enabled encryption on bucket 'serverless-image-handler-logs-[a-z0-9]{8}'/) ); expect(consoleInfoSpy).toHaveBeenCalledWith( - expect.stringMatching(/^Successfully added policy added to bucket 'serverless-image-handler-logs-[a-z0-9]{8}'/) + expect.stringMatching(/^Successfully added policy to bucket 'serverless-image-handler-logs-[a-z0-9]{8}'/) + ); + expect(consoleInfoSpy).toHaveBeenCalledWith( + expect.stringMatching(/^Successfully added tag to bucket 'serverless-image-handler-logs-[a-z0-9]{8}'/) ); }); @@ -136,7 +149,7 @@ describe("CREATE_LOGGING_BUCKET", () => { expect(consoleInfoSpy).toHaveBeenCalledWith( expect.stringMatching( - /^Successfully created bucket 'serverless-image-handler-logs-[a-z0-9]{8}' in 'us-east-1' region/ + /^Successfully created bucket 'serverless-image-handler-logs-[a-z0-9]{8}' in 'mock-region-1' region/ ) ); expect(consoleErrorSpy).toHaveBeenCalledWith( @@ -181,7 +194,7 @@ describe("CREATE_LOGGING_BUCKET", () => { expect(consoleInfoSpy).toHaveBeenCalledWith( expect.stringMatching( - /^Successfully created bucket 'serverless-image-handler-logs-[a-z0-9]{8}' in 'us-east-1' region/ + /^Successfully created bucket 'serverless-image-handler-logs-[a-z0-9]{8}' in 'mock-region-1' region/ ) ); expect(consoleInfoSpy).toHaveBeenCalledWith( @@ -200,4 +213,45 @@ describe("CREATE_LOGGING_BUCKET", () => { }, }); }); + + it("Should log a failure when there is an error adding a tag to the created bucket", async () => { + mockAwsEc2.describeRegions.mockImplementationOnce(() => ({ + promise() { + return Promise.resolve({ Regions: [{ RegionName: "mock-region-1" }] }); + }, + })); + mockAwsS3.createBucket.mockImplementation(() => ({ + promise() { + return Promise.resolve(); + }, + })); + mockAwsS3.putBucketEncryption.mockImplementation(() => ({ + promise() { + return Promise.resolve(); + }, + })); + mockAwsS3.putBucketPolicy.mockImplementation(() => ({ + promise() { + return Promise.resolve(); + }, + })); + mockAwsS3.putBucketTagging.mockImplementation(() => ({ + promise() { + return Promise.reject(new CustomResourceError(null, "putBucketTagging failed")); + }, + })); + + await handler(event, mockContext); + + expect.assertions(2); + + expect(consoleInfoSpy).toHaveBeenCalledWith( + expect.stringMatching( + /^Successfully created bucket 'serverless-image-handler-logs-[a-z0-9]{8}' in 'us-east-1' region/ + ) + ); + expect(consoleErrorSpy).toHaveBeenCalledWith( + expect.stringMatching(/^Failed to add tag to bucket 'serverless-image-handler-logs-[a-z0-9]{8}'/) + ); + }); }); diff --git a/source/custom-resource/test/mock.ts b/source/custom-resource/test/mock.ts index f66c2f83e..14c8134cf 100644 --- a/source/custom-resource/test/mock.ts +++ b/source/custom-resource/test/mock.ts @@ -18,6 +18,7 @@ export const mockAwsS3 = { createBucket: jest.fn(), putBucketEncryption: jest.fn(), putBucketPolicy: jest.fn(), + putBucketTagging: jest.fn(), }; jest.mock("aws-sdk/clients/s3", () => jest.fn(() => ({ ...mockAwsS3 }))); diff --git a/source/demo-ui/index.html b/source/demo-ui/index.html index 1b00c69aa..f843a0f5b 100644 --- a/source/demo-ui/index.html +++ b/source/demo-ui/index.html @@ -9,17 +9,10 @@