From b800c91a9b162b4ae1e55ac0691d6ae1a586af3c Mon Sep 17 00:00:00 2001 From: Ayush Goyal Date: Wed, 24 Nov 2021 22:48:30 +0530 Subject: [PATCH] feat(cloudfront): Add support for response headers policy (#17359) feat(cloudfront): Add support for response headers policy closes #17290 Notes: ~1. Currently the CFNSpec is not up-to-date with the latest available cloudformation changes for `ResponseHeadersPolicyId` in `AWS::CloudFront::Distribution CacheBehavior`. Some aspects of the same are added to the PR but are left commented. Would update the PR once the spec is updated.~ Refs: 1. https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/adding-response-headers.html 2. https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-cloudfront-responseheaderspolicy.html 3. https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cloudfront-distribution-cachebehavior.html#cfn-cloudfront-distribution-cachebehavior-responseheaderspolicyid ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-cloudfront/README.md | 52 ++ .../aws-cloudfront/lib/distribution.ts | 8 + packages/@aws-cdk/aws-cloudfront/lib/index.ts | 1 + .../lib/private/cache-behavior.ts | 1 + .../lib/response-headers-policy.ts | 456 ++++++++++++++++++ packages/@aws-cdk/aws-cloudfront/package.json | 5 +- .../integ.distribution-policies.expected.json | 41 +- .../test/integ.distribution-policies.ts | 14 + .../test/response-headers-policy.test.ts | 146 ++++++ 9 files changed, 722 insertions(+), 2 deletions(-) create mode 100644 packages/@aws-cdk/aws-cloudfront/lib/response-headers-policy.ts create mode 100644 packages/@aws-cdk/aws-cloudfront/test/response-headers-policy.test.ts diff --git a/packages/@aws-cdk/aws-cloudfront/README.md b/packages/@aws-cdk/aws-cloudfront/README.md index e10f5c39d596b..e87a4f9b56617 100644 --- a/packages/@aws-cdk/aws-cloudfront/README.md +++ b/packages/@aws-cdk/aws-cloudfront/README.md @@ -260,6 +260,58 @@ new cloudfront.Distribution(this, 'myDistCustomPolicy', { }); ``` +### Customizing Response Headers with Response Headers Policies + +You can configure CloudFront to add one or more HTTP headers to the responses that it sends to viewers (web browsers or other clients), without making any changes to the origin or writing any code. +To specify the headers that CloudFront adds to HTTP responses, you use a response headers policy. CloudFront adds the headers regardless of whether it serves the object from the cache or has to retrieve the object from the origin. If the origin response includes one or more of the headers that’s in a response headers policy, the policy can specify whether CloudFront uses the header it received from the origin or overwrites it with the one in the policy. +See https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/adding-response-headers.html + +```ts +// Using an existing managed response headers policy +declare const bucketOrigin: origins.S3Origin; +new cloudfront.Distribution(this, 'myDistManagedPolicy', { + defaultBehavior: { + origin: bucketOrigin, + responseHeadersPolicy: cloudfront.ResponseHeadersPolicy.CORS_ALLOW_ALL_ORIGINS, + }, +}); + +// Creating a custom response headers policy -- all parameters optional +const myResponseHeadersPolicy = new cloudfront.ResponseHeadersPolicy(this, 'ResponseHeadersPolicy', { + responseHeadersPolicyName: 'MyPolicy', + comment: 'A default policy', + corsBehavior: { + accessControlAllowCredentials: false, + accessControlAllowHeaders: ['X-Custom-Header-1', 'X-Custom-Header-2'], + accessControlAllowMethods: ['GET', 'POST'], + accessControlAllowOrigins: ['*'], + accessControlExposeHeaders: ['X-Custom-Header-1', 'X-Custom-Header-2'], + accessControlMaxAge: Duration.seconds(600), + originOverride: true, + }, + customHeadersBehavior: { + customHeaders: [ + { header: 'X-Amz-Date', value: 'some-value', override: true }, + { header: 'X-Amz-Security-Token', value: 'some-value', override: false }, + ], + }, + securityHeadersBehavior: { + contentSecurityPolicy: { contentSecurityPolicy: 'default-src https:;', override: true }, + contentTypeOptions: { override: true }, + frameOptions: { frameOption: cloudfront.HeadersFrameOption.DENY, override: true }, + referrerPolicy: { referrerPolicy: cloudfront.HeadersReferrerPolicy.NO_REFERRER, override: true }, + strictTransportSecurity: { accessControlMaxAge: Duration.seconds(600), includeSubdomains: true, override: true }, + xssProtection: { protection: true, modeBlock: true, reportUri: 'https://example.com/csp-report', override: true }, + }, +}); +new cloudfront.Distribution(this, 'myDistCustomPolicy', { + defaultBehavior: { + origin: bucketOrigin, + responseHeadersPolicy: myResponseHeadersPolicy, + }, +}); +``` + ### Validating signed URLs or signed cookies with Trusted Key Groups CloudFront Distribution supports validating signed URLs or signed cookies using key groups. diff --git a/packages/@aws-cdk/aws-cloudfront/lib/distribution.ts b/packages/@aws-cdk/aws-cloudfront/lib/distribution.ts index 517a73bcc8917..c593edd9efec7 100644 --- a/packages/@aws-cdk/aws-cloudfront/lib/distribution.ts +++ b/packages/@aws-cdk/aws-cloudfront/lib/distribution.ts @@ -12,6 +12,7 @@ import { IKeyGroup } from './key-group'; import { IOrigin, OriginBindConfig, OriginBindOptions } from './origin'; import { IOriginRequestPolicy } from './origin-request-policy'; import { CacheBehavior } from './private/cache-behavior'; +import { IResponseHeadersPolicy } from './response-headers-policy'; // v2 - keep this import as a separate section to reduce merge conflict when forward merging with the v2 branch. // eslint-disable-next-line @@ -700,6 +701,13 @@ export interface AddBehaviorOptions { */ readonly originRequestPolicy?: IOriginRequestPolicy; + /** + * The response headers policy for this behavior. The response headers policy determines which headers are included in responses + * + * @default - none + */ + readonly responseHeadersPolicy?: IResponseHeadersPolicy; + /** * Set this to true to indicate you want to distribute media files in the Microsoft Smooth Streaming format using this behavior. * diff --git a/packages/@aws-cdk/aws-cloudfront/lib/index.ts b/packages/@aws-cdk/aws-cloudfront/lib/index.ts index 74b5b4644919c..e5abe99c0bfbd 100644 --- a/packages/@aws-cdk/aws-cloudfront/lib/index.ts +++ b/packages/@aws-cdk/aws-cloudfront/lib/index.ts @@ -7,6 +7,7 @@ export * from './origin'; export * from './origin-access-identity'; export * from './origin-request-policy'; export * from './public-key'; +export * from './response-headers-policy'; export * from './web-distribution'; export * as experimental from './experimental'; diff --git a/packages/@aws-cdk/aws-cloudfront/lib/private/cache-behavior.ts b/packages/@aws-cdk/aws-cloudfront/lib/private/cache-behavior.ts index 4e6f71589bb7e..43d31bd976f40 100644 --- a/packages/@aws-cdk/aws-cloudfront/lib/private/cache-behavior.ts +++ b/packages/@aws-cdk/aws-cloudfront/lib/private/cache-behavior.ts @@ -48,6 +48,7 @@ export class CacheBehavior { cachePolicyId: (this.props.cachePolicy ?? CachePolicy.CACHING_OPTIMIZED).cachePolicyId, compress: this.props.compress ?? true, originRequestPolicyId: this.props.originRequestPolicy?.originRequestPolicyId, + responseHeadersPolicyId: this.props.responseHeadersPolicy?.responseHeadersPolicyId, smoothStreaming: this.props.smoothStreaming, viewerProtocolPolicy: this.props.viewerProtocolPolicy ?? ViewerProtocolPolicy.ALLOW_ALL, functionAssociations: this.props.functionAssociations?.map(association => ({ diff --git a/packages/@aws-cdk/aws-cloudfront/lib/response-headers-policy.ts b/packages/@aws-cdk/aws-cloudfront/lib/response-headers-policy.ts new file mode 100644 index 0000000000000..4e974454b3925 --- /dev/null +++ b/packages/@aws-cdk/aws-cloudfront/lib/response-headers-policy.ts @@ -0,0 +1,456 @@ +import { Duration, Names, Resource } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import { CfnResponseHeadersPolicy } from './cloudfront.generated'; + +/** + * Represents a response headers policy. + */ +export interface IResponseHeadersPolicy { + /** + * The ID of the response headers policy + * @attribute + **/ + readonly responseHeadersPolicyId: string; +} + +/** + * Properties for creating a Response Headers Policy + */ +export interface ResponseHeadersPolicyProps { + /** + * A unique name to identify the response headers policy. + * + * @default - generated from the `id` + */ + readonly responseHeadersPolicyName?: string; + + /** + * A comment to describe the response headers policy. + * + * @default - no comment + */ + readonly comment?: string; + + /** + * A configuration for a set of HTTP response headers that are used for cross-origin resource sharing (CORS). + * + * @default - no cors behavior + */ + readonly corsBehavior?: ResponseHeadersCorsBehavior; + + /** + * A configuration for a set of custom HTTP response headers. + * + * @default - no custom headers behavior + */ + readonly customHeadersBehavior?: ResponseCustomHeadersBehavior; + + /** + * A configuration for a set of security-related HTTP response headers. + * + * @default - no security headers behavior + */ + readonly securityHeadersBehavior?: ResponseSecurityHeadersBehavior; +} + +/** + * A Response Headers Policy configuration + * + * @resource AWS::CloudFront::ResponseHeadersPolicy + */ +export class ResponseHeadersPolicy extends Resource implements IResponseHeadersPolicy { + + /** Use this managed policy to allow simple CORS requests from any origin. */ + public static readonly CORS_ALLOW_ALL_ORIGINS = ResponseHeadersPolicy.fromManagedResponseHeadersPolicy('60669652-455b-4ae9-85a4-c4c02393f86c'); + /** Use this managed policy to allow CORS requests from any origin, including preflight requests. */ + public static readonly CORS_ALLOW_ALL_ORIGINS_WITH_PREFLIGHT = ResponseHeadersPolicy.fromManagedResponseHeadersPolicy('5cc3b908-e619-4b99-88e5-2cf7f45965bd'); + /** Use this managed policy to add a set of security headers to all responses that CloudFront sends to viewers. */ + public static readonly SECURITY_HEADERS = ResponseHeadersPolicy.fromManagedResponseHeadersPolicy('67f7725c-6f97-4210-82d7-5512b31e9d03'); + /** Use this managed policy to allow simple CORS requests from any origin and add a set of security headers to all responses that CloudFront sends to viewers. */ + public static readonly CORS_ALLOW_ALL_ORIGINS_AND_SECURITY_HEADERS = ResponseHeadersPolicy.fromManagedResponseHeadersPolicy('e61eb60c-9c35-4d20-a928-2b84e02af89c'); + /** Use this managed policy to allow CORS requests from any origin, including preflight requests, and add a set of security headers to all responses that CloudFront sends to viewers. */ + public static readonly CORS_ALLOW_ALL_ORIGINS_WITH_PREFLIGHT_AND_SECURITY_HEADERS = ResponseHeadersPolicy.fromManagedResponseHeadersPolicy('eaab4381-ed33-4a86-88ca-d9558dc6cd63'); + + /** + * Import an existing Response Headers Policy from its ID. + */ + public static fromResponseHeadersPolicyId(scope: Construct, id: string, responseHeadersPolicyId: string): IResponseHeadersPolicy { + class Import extends Resource implements IResponseHeadersPolicy { + public readonly responseHeadersPolicyId = responseHeadersPolicyId; + } + return new Import(scope, id); + } + + private static fromManagedResponseHeadersPolicy(managedResponseHeadersPolicyId: string): IResponseHeadersPolicy { + return new class implements IResponseHeadersPolicy { + public readonly responseHeadersPolicyId = managedResponseHeadersPolicyId; + }; + } + + public readonly responseHeadersPolicyId: string; + + constructor(scope: Construct, id: string, props: ResponseHeadersPolicyProps = {}) { + super(scope, id, { + physicalName: props.responseHeadersPolicyName, + }); + + const responseHeadersPolicyName = props.responseHeadersPolicyName ?? Names.uniqueId(this); + + const resource = new CfnResponseHeadersPolicy(this, 'Resource', { + responseHeadersPolicyConfig: { + name: responseHeadersPolicyName, + comment: props.comment, + corsConfig: props.corsBehavior ? this._renderCorsConfig(props.corsBehavior) : undefined, + customHeadersConfig: props.customHeadersBehavior ? this._renderCustomHeadersConfig(props.customHeadersBehavior) : undefined, + securityHeadersConfig: props.securityHeadersBehavior ? this._renderSecurityHeadersConfig(props.securityHeadersBehavior) : undefined, + }, + }); + + this.responseHeadersPolicyId = resource.ref; + } + + private _renderCorsConfig(behavior: ResponseHeadersCorsBehavior): CfnResponseHeadersPolicy.CorsConfigProperty { + return { + accessControlAllowCredentials: behavior.accessControlAllowCredentials, + accessControlAllowHeaders: { items: behavior.accessControlAllowHeaders }, + accessControlAllowMethods: { items: behavior.accessControlAllowMethods }, + accessControlAllowOrigins: { items: behavior.accessControlAllowOrigins }, + accessControlExposeHeaders: behavior.accessControlExposeHeaders ? { items: behavior.accessControlExposeHeaders } : undefined, + accessControlMaxAgeSec: behavior.accessControlMaxAge ? behavior.accessControlMaxAge.toSeconds() : undefined, + originOverride: behavior.originOverride, + }; + } + + private _renderCustomHeadersConfig(behavior: ResponseCustomHeadersBehavior): CfnResponseHeadersPolicy.CustomHeadersConfigProperty { + return { + items: behavior.customHeaders, + }; + } + + private _renderSecurityHeadersConfig(behavior: ResponseSecurityHeadersBehavior): CfnResponseHeadersPolicy.SecurityHeadersConfigProperty { + return { + contentSecurityPolicy: behavior.contentSecurityPolicy, + contentTypeOptions: behavior.contentTypeOptions, + frameOptions: behavior.frameOptions, + referrerPolicy: behavior.referrerPolicy, + strictTransportSecurity: behavior.strictTransportSecurity ? { + ...behavior.strictTransportSecurity, + accessControlMaxAgeSec: behavior.strictTransportSecurity.accessControlMaxAge.toSeconds(), + }: undefined, + xssProtection: behavior.xssProtection, + }; + } +} + +/** + * Configuration for a set of HTTP response headers that are used for cross-origin resource sharing (CORS). + * CloudFront adds these headers to HTTP responses that it sends for CORS requests that match a cache behavior + * associated with this response headers policy. + */ +export interface ResponseHeadersCorsBehavior { + /** + * A Boolean that CloudFront uses as the value for the Access-Control-Allow-Credentials HTTP response header. + */ + readonly accessControlAllowCredentials: boolean; + + /** + * A list of HTTP header names that CloudFront includes as values for the Access-Control-Allow-Headers HTTP response header. + * You can specify `['*']` to allow all headers. + */ + readonly accessControlAllowHeaders: string[]; + + /** + * A list of HTTP methods that CloudFront includes as values for the Access-Control-Allow-Methods HTTP response header. + */ + readonly accessControlAllowMethods: string[]; + + /** + * A list of origins (domain names) that CloudFront can use as the value for the Access-Control-Allow-Origin HTTP response header. + * You can specify `['*']` to allow all origins. + */ + readonly accessControlAllowOrigins: string[]; + + /** + * A list of HTTP headers that CloudFront includes as values for the Access-Control-Expose-Headers HTTP response header. + * You can specify `['*']` to expose all headers. + * + * @default - no headers exposed + */ + readonly accessControlExposeHeaders?: string[]; + + /** + * A number that CloudFront uses as the value for the Access-Control-Max-Age HTTP response header. + * + * @default - no max age + */ + readonly accessControlMaxAge?: Duration; + + /** + * A Boolean that determines whether CloudFront overrides HTTP response headers received from the origin with the ones specified in this response headers policy. + */ + readonly originOverride: boolean; +} + +/** + * Configuration for a set of HTTP response headers that are sent for requests that match a cache behavior + * that’s associated with this response headers policy. + */ +export interface ResponseCustomHeadersBehavior { + /** + * The list of HTTP response headers and their values. + */ + readonly customHeaders: ResponseCustomHeader[]; +} + +/** + * An HTTP response header name and its value. + * CloudFront includes this header in HTTP responses that it sends for requests that match a cache behavior that’s associated with this response headers policy. + */ +export interface ResponseCustomHeader { + /** + * The HTTP response header name. + */ + readonly header: string; + + /** + * A Boolean that determines whether CloudFront overrides a response header with the same name + * received from the origin with the header specified here. + */ + readonly override: boolean; + + /** + * The value for the HTTP response header. + */ + readonly value: string; +} + +/** + * Configuration for a set of security-related HTTP response headers. + * CloudFront adds these headers to HTTP responses that it sends for requests that match a cache behavior + * associated with this response headers policy. + */ +export interface ResponseSecurityHeadersBehavior { + /** + * The policy directives and their values that CloudFront includes as values for the Content-Security-Policy HTTP response header. + * + * @default - no content security policy + */ + readonly contentSecurityPolicy?: ResponseHeadersContentSecurityPolicy; + + /** + * Determines whether CloudFront includes the X-Content-Type-Options HTTP response header with its value set to nosniff. + * + * @default - no content type options + */ + readonly contentTypeOptions?: ResponseHeadersContentTypeOptions; + + /** + * Determines whether CloudFront includes the X-Frame-Options HTTP response header and the header’s value. + * + * @default - no frame options + */ + readonly frameOptions?: ResponseHeadersFrameOptions; + + /** + * Determines whether CloudFront includes the Referrer-Policy HTTP response header and the header’s value. + * + * @default - no referrer policy + */ + readonly referrerPolicy?: ResponseHeadersReferrerPolicy; + + /** + * Determines whether CloudFront includes the Strict-Transport-Security HTTP response header and the header’s value. + * + * @default - no strict transport security + */ + readonly strictTransportSecurity?: ResponseHeadersStrictTransportSecurity; + + /** + * Determines whether CloudFront includes the X-XSS-Protection HTTP response header and the header’s value. + * + * @default - no xss protection + */ + readonly xssProtection?: ResponseHeadersXSSProtection; +} + +/** + * The policy directives and their values that CloudFront includes as values for the Content-Security-Policy HTTP response header. + */ +export interface ResponseHeadersContentSecurityPolicy { + /** + * The policy directives and their values that CloudFront includes as values for the Content-Security-Policy HTTP response header. + */ + readonly contentSecurityPolicy: string; + + /** + * A Boolean that determines whether CloudFront overrides the Content-Security-Policy HTTP response header + * received from the origin with the one specified in this response headers policy. + */ + readonly override: boolean; +} + +/** + * Determines whether CloudFront includes the X-Content-Type-Options HTTP response header with its value set to nosniff. + */ +export interface ResponseHeadersContentTypeOptions { + /** + * A Boolean that determines whether CloudFront overrides the X-Content-Type-Options HTTP response header + * received from the origin with the one specified in this response headers policy. + */ + readonly override: boolean; +} + +/** + * Determines whether CloudFront includes the X-Frame-Options HTTP response header and the header’s value. + */ +export interface ResponseHeadersFrameOptions { + /** + * The value of the X-Frame-Options HTTP response header. + */ + readonly frameOption: HeadersFrameOption; + + /** + * A Boolean that determines whether CloudFront overrides the X-Frame-Options HTTP response header + * received from the origin with the one specified in this response headers policy. + */ + readonly override: boolean; +} + +/** + * Determines whether CloudFront includes the Referrer-Policy HTTP response header and the header’s value. + */ +export interface ResponseHeadersReferrerPolicy { + /** + * The value of the Referrer-Policy HTTP response header. + */ + readonly referrerPolicy: HeadersReferrerPolicy; + + /** + * A Boolean that determines whether CloudFront overrides the Referrer-Policy HTTP response header + * received from the origin with the one specified in this response headers policy. + */ + readonly override: boolean; +} + +/** + * Determines whether CloudFront includes the Strict-Transport-Security HTTP response header and the header’s value. + */ +export interface ResponseHeadersStrictTransportSecurity { + /** + * A number that CloudFront uses as the value for the max-age directive in the Strict-Transport-Security HTTP response header. + */ + readonly accessControlMaxAge: Duration; + + /** + * A Boolean that determines whether CloudFront includes the includeSubDomains directive in the Strict-Transport-Security HTTP response header. + * + * @default false + */ + readonly includeSubdomains?: boolean; + + /** + * A Boolean that determines whether CloudFront overrides the Strict-Transport-Security HTTP response header + * received from the origin with the one specified in this response headers policy. + */ + readonly override: boolean; + + /** + * A Boolean that determines whether CloudFront includes the preload directive in the Strict-Transport-Security HTTP response header. + * + * @default false + */ + readonly preload?: boolean; +} + +/** + * Determines whether CloudFront includes the X-XSS-Protection HTTP response header and the header’s value. + */ +export interface ResponseHeadersXSSProtection { + /** + * A Boolean that determines whether CloudFront includes the mode=block directive in the X-XSS-Protection header. + * + * @default false + */ + readonly modeBlock?: boolean; + + /** + * A Boolean that determines whether CloudFront overrides the X-XSS-Protection HTTP response header + * received from the origin with the one specified in this response headers policy. + */ + readonly override: boolean; + + /** + * A Boolean that determines the value of the X-XSS-Protection HTTP response header. + * When this setting is true, the value of the X-XSS-Protection header is 1. + * When this setting is false, the value of the X-XSS-Protection header is 0. + */ + readonly protection: boolean; + + /** + * A reporting URI, which CloudFront uses as the value of the report directive in the X-XSS-Protection header. + * You cannot specify a ReportUri when ModeBlock is true. + * + * @default - no report uri + */ + readonly reportUri?: string; +} + +/** + * Enum representing possible values of the X-Frame-Options HTTP response header. + */ +export enum HeadersFrameOption { + /** + * The page can only be displayed in a frame on the same origin as the page itself. + */ + DENY = 'DENY', + + /** + * The page can only be displayed in a frame on the specified origin. + */ + SAMEORIGIN = 'SAMEORIGIN', +} + +/** + * Enum representing possible values of the Referrer-Policy HTTP response header. + */ +export enum HeadersReferrerPolicy { + /** + * The referrer policy is not set. + */ + NO_REFERRER = 'no-referrer', + + /** + * The referrer policy is no-referrer-when-downgrade. + */ + NO_REFERRER_WHEN_DOWNGRADE = 'no-referrer-when-downgrade', + + /** + * The referrer policy is origin. + */ + ORIGIN = 'origin', + + /** + * The referrer policy is origin-when-cross-origin. + */ + ORIGIN_WHEN_CROSS_ORIGIN = 'origin-when-cross-origin', + + /** + * The referrer policy is same-origin. + */ + SAME_ORIGIN = 'same-origin', + + /** + * The referrer policy is strict-origin. + */ + STRICT_ORIGIN = 'strict-origin', + + /** + * The referrer policy is strict-origin-when-cross-origin. + */ + STRICT_ORIGIN_WHEN_CROSS_ORIGIN = 'strict-origin-when-cross-origin', + + /** + * The referrer policy is unsafe-url. + */ + UNSAFE_URL = 'unsafe-url', +} diff --git a/packages/@aws-cdk/aws-cloudfront/package.json b/packages/@aws-cdk/aws-cloudfront/package.json index b0fdf92f48da2..145e02736b1f7 100644 --- a/packages/@aws-cdk/aws-cloudfront/package.json +++ b/packages/@aws-cdk/aws-cloudfront/package.json @@ -169,7 +169,10 @@ "resource-attribute:@aws-cdk/aws-cloudfront.KeyGroup.keyGroupLastModifiedTime", "resource-attribute:@aws-cdk/aws-cloudfront.PublicKey.publicKeyCreatedTime", "resource-attribute:@aws-cdk/aws-cloudfront.OriginAccessIdentity.cloudFrontOriginAccessIdentityId", - "resource-attribute:@aws-cdk/aws-cloudfront.Function.functionMetadataFunctionArn" + "resource-attribute:@aws-cdk/aws-cloudfront.Function.functionMetadataFunctionArn", + "construct-interface-extends-iconstruct:@aws-cdk/aws-cloudfront.IResponseHeadersPolicy", + "resource-interface-extends-resource:@aws-cdk/aws-cloudfront.IResponseHeadersPolicy", + "resource-attribute:@aws-cdk/aws-cloudfront.ResponseHeadersPolicy.responseHeadersPolicyLastModifiedTime" ] }, "awscdkio": { diff --git a/packages/@aws-cdk/aws-cloudfront/test/integ.distribution-policies.expected.json b/packages/@aws-cdk/aws-cloudfront/test/integ.distribution-policies.expected.json index bf8b74f747b46..a09b1d2e38f09 100644 --- a/packages/@aws-cdk/aws-cloudfront/test/integ.distribution-policies.expected.json +++ b/packages/@aws-cdk/aws-cloudfront/test/integ.distribution-policies.expected.json @@ -12,8 +12,8 @@ "CookiesConfig": { "CookieBehavior": "none" }, - "EnableAcceptEncodingGzip": false, "EnableAcceptEncodingBrotli": false, + "EnableAcceptEncodingGzip": false, "HeadersConfig": { "HeaderBehavior": "none" }, @@ -44,6 +44,42 @@ } } }, + "ResponseHeadersPolicy13DBF9E0": { + "Type": "AWS::CloudFront::ResponseHeadersPolicy", + "Properties": { + "ResponseHeadersPolicyConfig": { + "CorsConfig": { + "AccessControlAllowCredentials": false, + "AccessControlAllowHeaders": { + "Items": [ + "X-Custom-Header-1", + "X-Custom-Header-2" + ] + }, + "AccessControlAllowMethods": { + "Items": [ + "GET", + "POST" + ] + }, + "AccessControlAllowOrigins": { + "Items": [ + "*" + ] + }, + "AccessControlExposeHeaders": { + "Items": [ + "X-Custom-Header-1", + "X-Custom-Header-2" + ] + }, + "AccessControlMaxAgeSec": 600, + "OriginOverride": true + }, + "Name": "ACustomResponseHeadersPolicy" + } + } + }, "DistB3B78991": { "Type": "AWS::CloudFront::Distribution", "Properties": { @@ -56,6 +92,9 @@ "OriginRequestPolicyId": { "Ref": "OriginRequestPolicy3EFDB4FA" }, + "ResponseHeadersPolicyId": { + "Ref": "ResponseHeadersPolicy13DBF9E0" + }, "TargetOriginId": "integdistributionpoliciesDistOrigin17849EF2C", "ViewerProtocolPolicy": "allow-all" }, diff --git a/packages/@aws-cdk/aws-cloudfront/test/integ.distribution-policies.ts b/packages/@aws-cdk/aws-cloudfront/test/integ.distribution-policies.ts index 8da36a18129a4..681c579a2eb45 100644 --- a/packages/@aws-cdk/aws-cloudfront/test/integ.distribution-policies.ts +++ b/packages/@aws-cdk/aws-cloudfront/test/integ.distribution-policies.ts @@ -14,11 +14,25 @@ const originRequestPolicy = new cloudfront.OriginRequestPolicy(stack, 'OriginReq headerBehavior: cloudfront.OriginRequestHeaderBehavior.all('CloudFront-Forwarded-Proto'), }); +const responseHeadersPolicy = new cloudfront.ResponseHeadersPolicy(stack, 'ResponseHeadersPolicy', { + responseHeadersPolicyName: 'ACustomResponseHeadersPolicy', + corsBehavior: { + accessControlAllowCredentials: false, + accessControlAllowHeaders: ['X-Custom-Header-1', 'X-Custom-Header-2'], + accessControlAllowMethods: ['GET', 'POST'], + accessControlAllowOrigins: ['*'], + accessControlExposeHeaders: ['X-Custom-Header-1', 'X-Custom-Header-2'], + accessControlMaxAge: cdk.Duration.seconds(600), + originOverride: true, + }, +}); + new cloudfront.Distribution(stack, 'Dist', { defaultBehavior: { origin: new TestOrigin('www.example.com'), cachePolicy, originRequestPolicy, + responseHeadersPolicy, }, }); diff --git a/packages/@aws-cdk/aws-cloudfront/test/response-headers-policy.test.ts b/packages/@aws-cdk/aws-cloudfront/test/response-headers-policy.test.ts new file mode 100644 index 0000000000000..926b17d85a219 --- /dev/null +++ b/packages/@aws-cdk/aws-cloudfront/test/response-headers-policy.test.ts @@ -0,0 +1,146 @@ +import '@aws-cdk/assert-internal/jest'; +import { App, Duration, Stack } from '@aws-cdk/core'; +import { HeadersFrameOption, HeadersReferrerPolicy, ResponseHeadersPolicy } from '../lib'; + +describe('ResponseHeadersPolicy', () => { + let app: App; + let stack: Stack; + + beforeEach(() => { + app = new App(); + stack = new Stack(app, 'Stack', { + env: { account: '123456789012', region: 'testregion' }, + }); + }); + + test('import existing policy by id', () => { + const responseHeadersPolicyId = '344f6fe5-7ce5-4df0-a470-3f14177c549c'; + const responseHeadersPolicy = ResponseHeadersPolicy.fromResponseHeadersPolicyId(stack, 'MyPolicy', responseHeadersPolicyId); + expect(responseHeadersPolicy.responseHeadersPolicyId).toEqual(responseHeadersPolicyId); + }); + + test('managed policies are provided', () => { + expect(ResponseHeadersPolicy.CORS_ALLOW_ALL_ORIGINS.responseHeadersPolicyId).toEqual('60669652-455b-4ae9-85a4-c4c02393f86c'); + expect(ResponseHeadersPolicy.CORS_ALLOW_ALL_ORIGINS_WITH_PREFLIGHT.responseHeadersPolicyId).toEqual('5cc3b908-e619-4b99-88e5-2cf7f45965bd'); + expect(ResponseHeadersPolicy.SECURITY_HEADERS.responseHeadersPolicyId).toEqual('67f7725c-6f97-4210-82d7-5512b31e9d03'); + expect(ResponseHeadersPolicy.CORS_ALLOW_ALL_ORIGINS_AND_SECURITY_HEADERS.responseHeadersPolicyId).toEqual('e61eb60c-9c35-4d20-a928-2b84e02af89c'); + expect(ResponseHeadersPolicy.CORS_ALLOW_ALL_ORIGINS_WITH_PREFLIGHT_AND_SECURITY_HEADERS.responseHeadersPolicyId).toEqual('eaab4381-ed33-4a86-88ca-d9558dc6cd63'); + }); + + test('minimal example', () => { + new ResponseHeadersPolicy(stack, 'ResponseHeadersPolicy'); + + expect(stack).toHaveResource('AWS::CloudFront::ResponseHeadersPolicy', { + ResponseHeadersPolicyConfig: { + Name: 'StackResponseHeadersPolicy7B76F936', + }, + }); + }); + + test('maximum example', () => { + new ResponseHeadersPolicy(stack, 'ResponseHeadersPolicy', { + responseHeadersPolicyName: 'MyPolicy', + comment: 'A default policy', + corsBehavior: { + accessControlAllowCredentials: false, + accessControlAllowHeaders: ['X-Custom-Header-1', 'X-Custom-Header-2'], + accessControlAllowMethods: ['GET', 'POST'], + accessControlAllowOrigins: ['*'], + accessControlExposeHeaders: ['X-Custom-Header-1', 'X-Custom-Header-2'], + accessControlMaxAge: Duration.seconds(600), + originOverride: true, + }, + customHeadersBehavior: { + customHeaders: [ + { header: 'X-Custom-Header-1', value: 'application/json', override: true }, + { header: 'X-Custom-Header-2', value: '0', override: false }, + ], + }, + securityHeadersBehavior: { + contentSecurityPolicy: { contentSecurityPolicy: 'default-src https:;', override: true }, + contentTypeOptions: { override: true }, + frameOptions: { frameOption: HeadersFrameOption.DENY, override: true }, + referrerPolicy: { referrerPolicy: HeadersReferrerPolicy.NO_REFERRER, override: true }, + strictTransportSecurity: { accessControlMaxAge: Duration.seconds(600), includeSubdomains: true, override: true }, + xssProtection: { protection: true, modeBlock: true, reportUri: 'https://example.com/csp-report', override: true }, + }, + }); + + expect(stack).toHaveResource('AWS::CloudFront::ResponseHeadersPolicy', { + ResponseHeadersPolicyConfig: { + Comment: 'A default policy', + CorsConfig: { + AccessControlAllowCredentials: false, + AccessControlAllowHeaders: { + Items: [ + 'X-Custom-Header-1', + 'X-Custom-Header-2', + ], + }, + AccessControlAllowMethods: { + Items: [ + 'GET', + 'POST', + ], + }, + AccessControlAllowOrigins: { + Items: [ + '*', + ], + }, + AccessControlExposeHeaders: { + Items: [ + 'X-Custom-Header-1', + 'X-Custom-Header-2', + ], + }, + AccessControlMaxAgeSec: 600, + OriginOverride: true, + }, + CustomHeadersConfig: { + Items: [ + { + Header: 'X-Custom-Header-1', + Override: true, + Value: 'application/json', + }, + { + Header: 'X-Custom-Header-2', + Override: false, + Value: '0', + }, + ], + }, + Name: 'MyPolicy', + SecurityHeadersConfig: { + ContentSecurityPolicy: { + ContentSecurityPolicy: 'default-src https:;', + Override: true, + }, + ContentTypeOptions: { + Override: true, + }, + FrameOptions: { + FrameOption: 'DENY', + Override: true, + }, + ReferrerPolicy: { + Override: true, + ReferrerPolicy: 'no-referrer', + }, + StrictTransportSecurity: { + AccessControlMaxAgeSec: 600, + IncludeSubdomains: true, + Override: true, + }, + XSSProtection: { + ModeBlock: true, + Override: true, + Protection: true, + ReportUri: 'https://example.com/csp-report', + }, + }, + }, + }); + }); +});