Skip to content

Commit

Permalink
feat(s3): add CORS Property to S3 Bucket (#2101) (#2843)
Browse files Browse the repository at this point in the history
Add CORS Property to S3 Bucket for configuring bucket cross-origin
access rules.
  • Loading branch information
workeitel authored and rix0rrr committed Jun 17, 2019
1 parent beaf03d commit 1a386d8
Show file tree
Hide file tree
Showing 2 changed files with 227 additions and 1 deletion.
107 changes: 106 additions & 1 deletion packages/@aws-cdk/aws-s3/lib/bucket.ts
Expand Up @@ -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.
Expand Down Expand Up @@ -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[];
}

/**
Expand Down Expand Up @@ -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, {
Expand All @@ -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);
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -1103,6 +1189,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;
Expand Down
121 changes: 121 additions & 0 deletions 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();
},
};

0 comments on commit 1a386d8

Please sign in to comment.