diff --git a/package-lock.json b/package-lock.json index b0132bd1..1d3acb3b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7870,7 +7870,7 @@ }, "packages/static-hosting": { "name": "@aligent/cdk-static-hosting", - "version": "0.1.5", + "version": "0.1.13", "license": "GPL-3.0-only", "dependencies": { "@aws-cdk/aws-cloudfront": "1.180.0", diff --git a/packages/static-hosting/README.md b/packages/static-hosting/README.md index cbdf2d77..f105b757 100644 --- a/packages/static-hosting/README.md +++ b/packages/static-hosting/README.md @@ -2,10 +2,11 @@ ## Overview -This repository defines a CDK construct for hosting a static website on AWS using S3 and CloudFront. +This repository defines a CDK construct for hosting a static website on AWS using S3 and CloudFront. It can be imported and used within CDK applications. ## Example + The following CDK snippet can be used to provision a static hosting stack using this construct. ``` @@ -43,3 +44,46 @@ new HostingStack(app, 'hosting-stack', { }); ``` + +### Response Header Policies + +You can initialize [Response Headers Policies], map them and pass to the construct. + +1. Create a policy + + ```sh + // Creating a custom response headers policy -- all parameters optional + const reportUriPolicy = new ResponseHeadersPolicy(this, 'ReportUriPolicy', { + responseHeadersPolicyName: 'ReportUriPolicy', + comment: 'To enable CSP Reporting', + customHeadersBehavior: { + customHeaders: [ + { + header: 'content-security-policy-report-only', + value: `default-src 'none'; form-action 'none'; frame-ancestors 'none'; report-uri https://some-report-uri-domain.report-uri.com/r/t/csp/wizard`, + override: true + }, + ], + }, + }); + ``` + +2. Attached policy to desired cache behavior or path + + ```sh + const responseHeaders: ResponseHeaderMappings[] = [{ + header: reportUriPolicy, + pathPatterns: ['/au*', '/nz*'] + attachToDefault: false + }]; + ``` + + If you should attached the policy to the Default Behavior, set `attachToDefault: true` + +3. Include the config as props + + ```sh + new StaticHosting(this, 'pwa-stack', {...staticProps, ...{behaviors, customOriginConfigs, responseHeaders}}); + ``` + +[Response Headers Policies]:https://docs.aws.amazon.com/cdk/api/v1/docs/@aws-cdk_aws-cloudfront.ResponseHeadersPolicy.html diff --git a/packages/static-hosting/lib/static-hosting.ts b/packages/static-hosting/lib/static-hosting.ts index e44e190a..03101dcf 100644 --- a/packages/static-hosting/lib/static-hosting.ts +++ b/packages/static-hosting/lib/static-hosting.ts @@ -68,6 +68,11 @@ export interface StaticHostingProps { * Optional WAF ARN */ webAclArn?: string; + + /** + * Response Header policies + */ + responseHeadersPolicies?: ResponseHeaderMappings[]; } interface remapPath { @@ -75,6 +80,12 @@ interface remapPath { to: string } +export interface ResponseHeaderMappings { + header: ResponseHeadersPolicy, + pathPatterns: string[], + attachToDefault?: boolean +} + export class StaticHosting extends Construct { private staticFiles = ["js", "css", "json", "svg", "jpg", "jpeg", "png"]; @@ -303,6 +314,69 @@ export class StaticHosting extends Construct { }); } + /** + * Response Header policies + * This feature helps to attached custom ResponseHeadersPolicies to + * the cache behaviors + */ + if (props.responseHeadersPolicies) { + const cfnDistribution = distribution.node.defaultChild as CfnDistribution; + + /** + * If we prepend custom origin configs, + * it would change the array indexes. + */ + let numberOfCustomBehaviors = 0; + if (props.prependCustomOriginBehaviours) { + numberOfCustomBehaviors = props.customOriginConfigs?.reduce((acc, current) => acc + current.behaviors.length, 0)!; + } + + props.responseHeadersPolicies.forEach( (policyMapping) => { + /** + * If the policy should be attached to default behavior + */ + if (policyMapping.attachToDefault) { + cfnDistribution.addOverride( + `Properties.DistributionConfig.` + + `DefaultCacheBehavior.` + + `ResponseHeadersPolicyId`, + policyMapping.header.responseHeadersPolicyId + ); + new CfnOutput(this, `response header policies ${policyMapping.header.node.id} default`, { + description: `response header policy mappings`, + value: `{ path: "default", policy: "${policyMapping.header.responseHeadersPolicyId}" }`, + exportName: `${exportPrefix}HeaderPolicy-default` + }); + }; + /** + * If the policy should be attached to + * specified path patterns + */ + policyMapping.pathPatterns.forEach(path => { + /** + * Looking for the index of the behavior + * according to the path pattern + * If the path patter is not found, it would be ignored + */ + let behaviorIndex = props.behaviors?.findIndex(behavior => {return behavior.pathPattern === path})! + numberOfCustomBehaviors; + + if (behaviorIndex >= numberOfCustomBehaviors){ + cfnDistribution.addOverride( + `Properties.DistributionConfig.CacheBehaviors.` + + `${behaviorIndex}` + + `.ResponseHeadersPolicyId`, + policyMapping.header.responseHeadersPolicyId + ); + new CfnOutput(this, `response header policies ${policyMapping.header.node.id} ${path.replace(/\W/g, '')}`, { + description: `response header policy mappings`, + value: `{ path: "${path}", policy: "${policyMapping.header.responseHeadersPolicyId}"}`, + exportName: `${exportPrefix}HeaderPolicy-${path.replace(/\W/g, '')}` + }); + }; + }); + }); + } + if (publisherGroup) { const cloudFrontInvalidationPolicyStatement = new PolicyStatement({ effect: Effect.ALLOW, diff --git a/packages/static-hosting/package.json b/packages/static-hosting/package.json index 3144ead8..5992ce9e 100644 --- a/packages/static-hosting/package.json +++ b/packages/static-hosting/package.json @@ -1,6 +1,6 @@ { "name": "@aligent/cdk-static-hosting", - "version": "0.1.5", + "version": "0.1.13", "main": "index.js", "license": "GPL-3.0-only", "homepage": "https://github.com/aligent/aws-cdk-static-hosting-stack#readme",