From 69b8d6dc212762ac8cafca35b3a81ccb5ea6a0d4 Mon Sep 17 00:00:00 2001 From: fargito Date: Fri, 6 May 2022 17:57:41 +0200 Subject: [PATCH 1/3] fix(static-website): re-enable static website hosting --- src/constructs/aws/StaticWebsite.ts | 19 ++++++++++++ .../aws/abstracts/StaticWebsiteAbstract.ts | 31 ++++++++++++++----- 2 files changed, 42 insertions(+), 8 deletions(-) diff --git a/src/constructs/aws/StaticWebsite.ts b/src/constructs/aws/StaticWebsite.ts index b98dc144..ae63f4c1 100644 --- a/src/constructs/aws/StaticWebsite.ts +++ b/src/constructs/aws/StaticWebsite.ts @@ -2,6 +2,8 @@ import * as cloudfront from "aws-cdk-lib/aws-cloudfront"; import { FunctionEventType } from "aws-cdk-lib/aws-cloudfront"; import type { Construct as CdkConstruct } from "constructs"; import type { AwsProvider } from "@lift/providers"; +import type { BucketProps } from "aws-cdk-lib/aws-s3"; +import { RemovalPolicy } from "aws-cdk-lib"; import { redirectToMainDomain } from "../../classes/cloudfrontFunctions"; import { getCfnFunctionAssociations } from "../../utils/getDefaultCfnFunctionAssociations"; import type { CommonStaticWebsiteConfiguration } from "./abstracts/StaticWebsiteAbstract"; @@ -55,4 +57,21 @@ export class StaticWebsite extends StaticWebsiteAbstract { code: cloudfront.FunctionCode.fromInline(code), }); } + /** + * Overrides the default `getBucketProps` from the abstract class + * + * @returns bucketProps + */ + + getBucketProps(): BucketProps { + return { + // Enable static website hosting + websiteIndexDocument: "index.html", + websiteErrorDocument: this.errorPath(), + // public read access is required when enabling static website hosting + publicReadAccess: true, + // For a static website, the content is code that should be versioned elsewhere + removalPolicy: RemovalPolicy.DESTROY, + }; + } } diff --git a/src/constructs/aws/abstracts/StaticWebsiteAbstract.ts b/src/constructs/aws/abstracts/StaticWebsiteAbstract.ts index 5b1aa4a1..58ca17a5 100644 --- a/src/constructs/aws/abstracts/StaticWebsiteAbstract.ts +++ b/src/constructs/aws/abstracts/StaticWebsiteAbstract.ts @@ -10,10 +10,11 @@ import { ViewerProtocolPolicy, } from "aws-cdk-lib/aws-cloudfront"; import { S3Origin } from "aws-cdk-lib/aws-cloudfront-origins"; +import type { BucketProps } from "aws-cdk-lib/aws-s3"; import { Bucket } from "aws-cdk-lib/aws-s3"; import type { Construct as CdkConstruct } from "constructs"; -import { Duration } from "aws-cdk-lib"; -import { CfnOutput, RemovalPolicy } from "aws-cdk-lib"; +import { Duration, RemovalPolicy } from "aws-cdk-lib"; +import { CfnOutput } from "aws-cdk-lib"; import type { ConstructCommands } from "@lift/constructs"; import { AwsConstruct } from "@lift/constructs/abstracts"; import type { AwsProvider } from "@lift/providers"; @@ -87,10 +88,9 @@ export abstract class StaticWebsiteAbstract extends AwsConstruct { ); } - const bucket = new Bucket(this, "Bucket", { - // For a static website, the content is code that should be versioned elsewhere - removalPolicy: RemovalPolicy.DESTROY, - }); + const bucketProps = this.getBucketProps(); + + const bucket = new Bucket(this, "Bucket", bucketProps); // Cast the domains to an array this.domains = configuration.domain !== undefined ? flatten([configuration.domain]) : undefined; @@ -266,8 +266,7 @@ export abstract class StaticWebsiteAbstract extends AwsConstruct { return this.provider.getStackOutput(this.distributionIdOutput); } - private errorResponse(): ErrorResponse { - // Custom error page + errorPath(): string | undefined { if (this.configuration.errorPage !== undefined) { let errorPath = this.configuration.errorPage; if (errorPath.startsWith("./") || errorPath.startsWith("../")) { @@ -281,6 +280,15 @@ export abstract class StaticWebsiteAbstract extends AwsConstruct { errorPath = `/${errorPath}`; } + return errorPath; + } + } + + private errorResponse(): ErrorResponse { + const errorPath = this.errorPath(); + + // Custom error page + if (errorPath !== undefined) { return { httpStatus: 404, ttl: Duration.seconds(0), @@ -326,4 +334,11 @@ export abstract class StaticWebsiteAbstract extends AwsConstruct { code: cloudfront.FunctionCode.fromInline(code), }); } + + getBucketProps(): BucketProps { + return { + // For a static website, the content is code that should be versioned elsewhere + removalPolicy: RemovalPolicy.DESTROY, + }; + } } From eb0cc4bad24748850709121ef7bcd6d267c0f16f Mon Sep 17 00:00:00 2001 From: fargito Date: Sun, 8 May 2022 22:58:21 +0200 Subject: [PATCH 2/3] test(static-website): edit static website tests --- ...Websites.test.ts => staticWebsite.test.ts} | 130 +++++++++++++----- 1 file changed, 99 insertions(+), 31 deletions(-) rename test/unit/{staticWebsites.test.ts => staticWebsite.test.ts} (81%) diff --git a/test/unit/staticWebsites.test.ts b/test/unit/staticWebsite.test.ts similarity index 81% rename from test/unit/staticWebsites.test.ts rename to test/unit/staticWebsite.test.ts index c60bfedb..d309e996 100644 --- a/test/unit/staticWebsites.test.ts +++ b/test/unit/staticWebsite.test.ts @@ -26,7 +26,6 @@ describe("static websites", () => { }); const bucketLogicalId = computeLogicalId("landing", "Bucket"); const bucketPolicyLogicalId = computeLogicalId("landing", "Bucket", "Policy"); - const originAccessIdentityLogicalId = computeLogicalId("landing", "CDN", "Origin1", "S3Origin"); const responseFunction = computeLogicalId("landing", "ResponseFunction"); const cfDistributionLogicalId = computeLogicalId("landing", "CDN"); const cfOriginId = computeLogicalId("landing", "CDN", "Origin1"); @@ -36,15 +35,20 @@ describe("static websites", () => { bucketLogicalId, bucketPolicyLogicalId, responseFunction, - originAccessIdentityLogicalId, cfDistributionLogicalId, ]); - expect(cfTemplate.Resources[bucketLogicalId]).toMatchObject({ + expect(cfTemplate.Resources[bucketLogicalId]).toStrictEqual({ Type: "AWS::S3::Bucket", UpdateReplacePolicy: "Delete", DeletionPolicy: "Delete", + Properties: { + WebsiteConfiguration: { + IndexDocument: "index.html", + }, + }, }); - expect(cfTemplate.Resources[bucketPolicyLogicalId]).toMatchObject({ + expect(cfTemplate.Resources[bucketPolicyLogicalId]).toStrictEqual({ + Type: "AWS::S3::BucketPolicy", Properties: { Bucket: { Ref: bucketLogicalId, @@ -55,9 +59,7 @@ describe("static websites", () => { Action: "s3:GetObject", Effect: "Allow", Principal: { - CanonicalUser: { - "Fn::GetAtt": [originAccessIdentityLogicalId, "S3CanonicalUserId"], - }, + AWS: "*", }, Resource: { "Fn::Join": ["", [{ "Fn::GetAtt": [bucketLogicalId, "Arn"] }, "/*"]] }, }, @@ -66,20 +68,14 @@ describe("static websites", () => { }, }, }); - expect(cfTemplate.Resources[originAccessIdentityLogicalId]).toMatchObject({ - Type: "AWS::CloudFront::CloudFrontOriginAccessIdentity", - Properties: { - CloudFrontOriginAccessIdentityConfig: { - Comment: `Identity for ${cfOriginId}`, - }, - }, - }); - expect(cfTemplate.Resources[cfDistributionLogicalId]).toMatchObject({ + expect(cfTemplate.Resources[cfDistributionLogicalId]).toStrictEqual({ Type: "AWS::CloudFront::Distribution", Properties: { DistributionConfig: { + Comment: "app-dev landing website CDN", CustomErrorResponses: [ { + // The response code is forced to 200 and changed to /index.html ErrorCachingMinTTL: 0, ErrorCode: 404, ResponseCode: 200, @@ -88,6 +84,7 @@ describe("static websites", () => { ], DefaultCacheBehavior: { AllowedMethods: ["GET", "HEAD", "OPTIONS"], + CachePolicyId: "658327ea-f89d-4fab-a63d-7e88639e58f6", Compress: true, TargetOriginId: cfOriginId, ViewerProtocolPolicy: "redirect-to-https", @@ -106,23 +103,24 @@ describe("static websites", () => { IPV6Enabled: true, Origins: [ { - DomainName: { - "Fn::GetAtt": [bucketLogicalId, "RegionalDomainName"], + CustomOriginConfig: { + OriginProtocolPolicy: "http-only", + OriginSSLProtocols: ["TLSv1.2"], }, - Id: cfOriginId, - S3OriginConfig: { - OriginAccessIdentity: { - "Fn::Join": [ - "", - [ - "origin-access-identity/cloudfront/", + DomainName: { + "Fn::Select": [ + 2, + { + "Fn::Split": [ + "/", { - Ref: originAccessIdentityLogicalId, + "Fn::GetAtt": [bucketLogicalId, "WebsiteURL"], }, ], - ], - }, + }, + ], }, + Id: cfOriginId, }, ], }, @@ -154,7 +152,7 @@ describe("static websites", () => { }, }, }); - expect(cfTemplate.Resources[responseFunction]).toMatchObject({ + expect(cfTemplate.Resources[responseFunction]).toStrictEqual({ Type: "AWS::CloudFront::Function", Properties: { AutoPublish: true, @@ -162,6 +160,24 @@ describe("static websites", () => { Comment: "app-dev-us-east-1-landing-response", Runtime: "cloudfront-js-1.0", }, + FunctionCode: `function handler(event) { + var response = event.response; + response.headers = Object.assign({}, { + "x-frame-options": { + "value": "SAMEORIGIN" + }, + "x-content-type-options": { + "value": "nosniff" + }, + "x-xss-protection": { + "value": "1; mode=block" + }, + "strict-transport-security": { + "value": "max-age=63072000" + } +}, response.headers); + return response; +}`, Name: "app-dev-us-east-1-landing-response", }, }); @@ -269,9 +285,10 @@ describe("static websites", () => { }), }); const edgeFunction = computeLogicalId("landing", "ResponseFunction"); - expect(cfTemplate.Resources[edgeFunction]).toMatchObject({ + expect(cfTemplate.Resources[edgeFunction]).toStrictEqual({ Type: "AWS::CloudFront::Function", Properties: { + AutoPublish: true, // Check that the `x-frame-options` header is not set FunctionCode: `function handler(event) { var response = event.response; @@ -288,6 +305,11 @@ describe("static websites", () => { }, response.headers); return response; }`, + FunctionConfig: { + Comment: "app-dev-us-east-1-landing-response", + Runtime: "cloudfront-js-1.0", + }, + Name: "app-dev-us-east-1-landing-response", }, }); }); @@ -386,9 +408,14 @@ describe("static websites", () => { }); const cfDistributionLogicalId = computeLogicalId("landing", "CDN"); - expect(cfTemplate.Resources[cfDistributionLogicalId]).toMatchObject({ + const bucketLogicalId = computeLogicalId("landing", "Bucket"); + const responseFunction = computeLogicalId("landing", "ResponseFunction"); + const cfOriginId = computeLogicalId("landing", "CDN", "Origin1"); + expect(cfTemplate.Resources[cfDistributionLogicalId]).toStrictEqual({ + Type: "AWS::CloudFront::Distribution", Properties: { DistributionConfig: { + Comment: "app-dev landing website CDN", CustomErrorResponses: [ { // The response code is forced to 404 and changed to /error.html @@ -398,6 +425,47 @@ describe("static websites", () => { ResponsePagePath: "/my/custom/error.html", }, ], + DefaultCacheBehavior: { + AllowedMethods: ["GET", "HEAD", "OPTIONS"], + CachePolicyId: "658327ea-f89d-4fab-a63d-7e88639e58f6", + Compress: true, + TargetOriginId: cfOriginId, + ViewerProtocolPolicy: "redirect-to-https", + FunctionAssociations: [ + { + EventType: "viewer-response", + FunctionARN: { + "Fn::GetAtt": [responseFunction, "FunctionARN"], + }, + }, + ], + }, + DefaultRootObject: "index.html", + Enabled: true, + HttpVersion: "http2", + IPV6Enabled: true, + Origins: [ + { + CustomOriginConfig: { + OriginProtocolPolicy: "http-only", + OriginSSLProtocols: ["TLSv1.2"], + }, + DomainName: { + "Fn::Select": [ + 2, + { + "Fn::Split": [ + "/", + { + "Fn::GetAtt": [bucketLogicalId, "WebsiteURL"], + }, + ], + }, + ], + }, + Id: cfOriginId, + }, + ], }, }, }); From a5350595c16e54d14dca43d10c4aa4fbbac8fa80 Mon Sep 17 00:00:00 2001 From: fargito Date: Fri, 13 May 2022 15:55:18 +0200 Subject: [PATCH 3/3] fix: remove jsdoc --- src/constructs/aws/StaticWebsite.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/constructs/aws/StaticWebsite.ts b/src/constructs/aws/StaticWebsite.ts index ae63f4c1..336135c9 100644 --- a/src/constructs/aws/StaticWebsite.ts +++ b/src/constructs/aws/StaticWebsite.ts @@ -57,11 +57,6 @@ export class StaticWebsite extends StaticWebsiteAbstract { code: cloudfront.FunctionCode.fromInline(code), }); } - /** - * Overrides the default `getBucketProps` from the abstract class - * - * @returns bucketProps - */ getBucketProps(): BucketProps { return {