From ba1a5fbe0411d8295ac1ccffb2d51be355be7834 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 | 107 +++++++++++++++++- packages/@aws-cdk/aws-s3/test/test.cors.ts | 121 +++++++++++++++++++++ 2 files changed, 227 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..37ee9563e4916 100644 --- a/packages/@aws-cdk/aws-s3/lib/bucket.ts +++ b/packages/@aws-cdk/aws-s3/lib/bucket.ts @@ -632,6 +632,70 @@ export interface BucketMetrics { readonly tagFilters?: {[tag: string]: any}; } +/** + * All http request methods + */ +export enum HttpMethods { + /** + * The GET method requests a representation of the specified resource. + */ + GET = "GET", + /** + * The PUT method replaces all current representations of the target resource with the request payload. + */ + PUT = "PUT", + /** + * The HEAD method asks for a response identical to that of a GET request, but without the response body. + */ + HEAD = "HEAD", + /** + * The POST method is used to submit an entity to the specified resource, often causing a change in state or side effects on the server. + */ + POST = "POST", + /** + * The DELETE method deletes the specified resource. + */ + DELETE = "DELETE", +} + +/** + * Specifies a cross-origin access rule for an Amazon S3 bucket. + */ +export interface CorsRule { + /** + * A unique identifier for this rule. + * + * @default - No id specified. + */ + readonly id?: string; + /** + * The time in seconds that your browser is to cache the preflight response for the specified resource. + * + * @default - No caching. + */ + readonly maxAge?: number; + /** + * Headers that are specified in the Access-Control-Request-Headers header. + * + * @default - No headers allowed. + */ + readonly allowedHeaders?: string[]; + /** + * An HTTP method that you allow the origin to execute. + */ + readonly allowedMethods: HttpMethods[]; + /** + * 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. + * + * @default - No headers exposed. + */ + readonly exposedHeaders?: string[]; +} + export interface BucketProps { /** * The kind of server-side encryption to apply to this bucket. @@ -725,6 +789,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 +881,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 +900,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 +930,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.addCorsRule.bind(this)); // Add all lifecycle rules (props.lifecycleRules || []).forEach(this.addLifecycleRule.bind(this)); @@ -892,6 +969,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 addCorsRule(rule: CorsRule) { + this.cors.push(rule); + } + /** * Adds a bucket notification event destination. * @param event The event to trigger the notification @@ -1096,6 +1182,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..461b0142d782b --- /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, HttpMethods } 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.addCorsRule({ + allowedMethods: [HttpMethods.GET, HttpMethods.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: [ + HttpMethods.GET + ], + allowedOrigins: [ + "*" + ], + exposedHeaders: [ + "Date" + ], + id: "myCORSRuleId1", + maxAge: 3600 + }, + { + allowedHeaders: [ + "x-amz-*" + ], + allowedMethods: [ + HttpMethods.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(); + }, +};