From b28c7ee38de84d19e0087c7925e9a9af3294e34f Mon Sep 17 00:00:00 2001 From: Florian Eitel Date: Wed, 12 Jun 2019 17:47:03 +0200 Subject: [PATCH] feat(s3): add CORS Property to S3 Bucket (#2101) Add CORS Property to S3 Bucket for configuring bucket cross-origin access rules. You can either specify the metrics as properties: new Bucket(stack, 'Bucket', { cors: [ { allowedHeaders: [ "*" ], allowedMethods: [ "GET" ], allowedOrigins: [ "*" ], exposedHeaders: [ "Date" ], id: "myCORSRuleId1", maxAge: 3600 } ] }); Or use the `addCors` function: const bucket = new Bucket(stack, 'Bucket'); bucket.addCors({ allowedMethods: ["GET", "HEAD"], allowedOrigins: ["https://example.com"] }); --- packages/@aws-cdk/aws-s3/lib/bucket.ts | 73 ++++++++++++- packages/@aws-cdk/aws-s3/test/test.cors.ts | 121 +++++++++++++++++++++ 2 files changed, 193 insertions(+), 1 deletion(-) create mode 100644 packages/@aws-cdk/aws-s3/test/test.cors.ts diff --git a/packages/@aws-cdk/aws-s3/lib/bucket.ts b/packages/@aws-cdk/aws-s3/lib/bucket.ts index 78aaf22b4839b..9201526bb5a25 100644 --- a/packages/@aws-cdk/aws-s3/lib/bucket.ts +++ b/packages/@aws-cdk/aws-s3/lib/bucket.ts @@ -632,6 +632,36 @@ export interface BucketMetrics { readonly tagFilters?: {[tag: string]: any}; } +/** + * Specifies a cross-origin access rule for an Amazon S3 bucket. + */ +export interface CorsRule { + /** + * A unique identifier for this rule. + */ + readonly id?: string; + /** + * The time in seconds that your browser is to cache the preflight response for the specified resource. + */ + readonly maxAge?: number; + /** + * Headers that are specified in the Access-Control-Request-Headers header. + */ + readonly allowedHeaders?: string[]; + /** + * An HTTP method that you allow the origin to execute. + */ + readonly allowedMethods: Array<"GET"|"PUT"|"HEAD"|"POST"|"DELETE">; + /** + * One or more origins you want customers to be able to access the bucket from. + */ + readonly allowedOrigins: string[]; + /** + * One or more headers in the response that you want customers to be able to access from their applications. + */ + readonly exposedHeaders?: string[]; +} + export interface BucketProps { /** * The kind of server-side encryption to apply to this bucket. @@ -725,6 +755,15 @@ export interface BucketProps { * @default - No metrics configuration. */ readonly metrics?: BucketMetrics[]; + + /** + * The CORS configuration of this bucket. + * + * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-s3-bucket-cors.html + * + * @default - No CORS configuration. + */ + readonly cors?: CorsRule[]; } /** @@ -808,6 +847,7 @@ export class Bucket extends BucketBase { private readonly versioned?: boolean; private readonly notifications: BucketNotifications; private readonly metrics: BucketMetrics[] = []; + private readonly cors: CorsRule[] = []; constructor(scope: Construct, id: string, props: BucketProps = {}) { super(scope, id, { @@ -826,7 +866,8 @@ export class Bucket extends BucketBase { lifecycleConfiguration: Lazy.anyValue({ produce: () => this.parseLifecycleConfiguration() }), websiteConfiguration: this.renderWebsiteConfiguration(props), publicAccessBlockConfiguration: props.blockPublicAccess, - metricsConfigurations: Lazy.anyValue({ produce: () => this.parseMetricConfiguration() }) + metricsConfigurations: Lazy.anyValue({ produce: () => this.parseMetricConfiguration() }), + corsConfiguration: Lazy.anyValue({ produce: () => this.parseCorsConfiguration() }) }); applyRemovalPolicy(resource, props.removalPolicy !== undefined ? props.removalPolicy : RemovalPolicy.Orphan); @@ -855,6 +896,8 @@ export class Bucket extends BucketBase { // Add all bucket metric configurations rules (props.metrics || []).forEach(this.addMetric.bind(this)); + // Add all cors configuration rules + (props.cors || []).forEach(this.addCors.bind(this)); // Add all lifecycle rules (props.lifecycleRules || []).forEach(this.addLifecycleRule.bind(this)); @@ -892,6 +935,15 @@ export class Bucket extends BucketBase { this.metrics.push(metric); } + /** + * Adds a cross-origin access configuration for objects in an Amazon S3 bucket + * + * @param rule The CORS configuration rule to add + */ + public addCors(rule: CorsRule) { + this.cors.push(rule); + } + /** * Adds a bucket notification event destination. * @param event The event to trigger the notification @@ -1096,6 +1148,25 @@ export class Bucket extends BucketBase { } } + private parseCorsConfiguration(): CfnBucket.CorsConfigurationProperty | undefined { + if (!this.cors || this.cors.length === 0) { + return undefined; + } + + return { corsRules: this.cors.map(parseCors) }; + + function parseCors(rule: CorsRule): CfnBucket.CorsRuleProperty { + return { + id: rule.id, + maxAge: rule.maxAge, + allowedHeaders: rule.allowedHeaders, + allowedMethods: rule.allowedMethods, + allowedOrigins: rule.allowedOrigins, + exposedHeaders: rule.exposedHeaders + }; + } + } + private parseTagFilters(tagFilters?: {[tag: string]: any}) { if (!tagFilters || tagFilters.length === 0) { return undefined; diff --git a/packages/@aws-cdk/aws-s3/test/test.cors.ts b/packages/@aws-cdk/aws-s3/test/test.cors.ts new file mode 100644 index 0000000000000..4fe81e5a32bf2 --- /dev/null +++ b/packages/@aws-cdk/aws-s3/test/test.cors.ts @@ -0,0 +1,121 @@ +import { expect, haveResource } from '@aws-cdk/assert'; +import { Stack } from '@aws-cdk/cdk'; +import { Test } from 'nodeunit'; +import { Bucket } from '../lib'; + +export = { + 'Can use addCors() to add a CORS configuration'(test: Test) { + // GIVEN + const stack = new Stack(); + + // WHEN + const bucket = new Bucket(stack, 'Bucket'); + bucket.addCors({ + allowedMethods: ["GET", "HEAD"], + allowedOrigins: ["https://example.com"] + }); + + // THEN + expect(stack).to(haveResource('AWS::S3::Bucket', { + CorsConfiguration: { + CorsRules: [{ + AllowedMethods: ["GET", "HEAD"], + AllowedOrigins: ["https://example.com"] + }] + } + })); + + test.done(); + }, + + 'Bucket with multiple cors configurations'(test: Test) { + // GIVEN + const stack = new Stack(); + + // WHEN + new Bucket(stack, 'Bucket', { + cors: [ + { + allowedHeaders: [ + "*" + ], + allowedMethods: [ + "GET" + ], + allowedOrigins: [ + "*" + ], + exposedHeaders: [ + "Date" + ], + id: "myCORSRuleId1", + maxAge: 3600 + }, + { + allowedHeaders: [ + "x-amz-*" + ], + allowedMethods: [ + "DELETE" + ], + allowedOrigins: [ + "http://www.example1.com", + "http://www.example2.com" + ], + exposedHeaders: [ + "Connection", + "Server", + "Date" + ], + id: "myCORSRuleId2", + maxAge: 1800 + } + ] + }); + + // THEN + expect(stack).to(haveResource('AWS::S3::Bucket', { + CorsConfiguration: { + CorsRules: [ + { + AllowedHeaders: [ + "*" + ], + AllowedMethods: [ + "GET" + ], + AllowedOrigins: [ + "*" + ], + ExposedHeaders: [ + "Date" + ], + Id: "myCORSRuleId1", + MaxAge: 3600 + }, + { + AllowedHeaders: [ + "x-amz-*" + ], + AllowedMethods: [ + "DELETE" + ], + AllowedOrigins: [ + "http://www.example1.com", + "http://www.example2.com" + ], + ExposedHeaders: [ + "Connection", + "Server", + "Date" + ], + Id: "myCORSRuleId2", + MaxAge: 1800 + } + ] + } + })); + + test.done(); + }, +};