Skip to content

Commit 1a386d8

Browse files
workeitelrix0rrr
authored andcommitted
feat(s3): add CORS Property to S3 Bucket (#2101) (#2843)
Add CORS Property to S3 Bucket for configuring bucket cross-origin access rules.
1 parent beaf03d commit 1a386d8

File tree

2 files changed

+227
-1
lines changed

2 files changed

+227
-1
lines changed

packages/@aws-cdk/aws-s3/lib/bucket.ts

Lines changed: 106 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -632,6 +632,70 @@ export interface BucketMetrics {
632632
readonly tagFilters?: {[tag: string]: any};
633633
}
634634

635+
/**
636+
* All http request methods
637+
*/
638+
export enum HttpMethods {
639+
/**
640+
* The GET method requests a representation of the specified resource.
641+
*/
642+
GET = "GET",
643+
/**
644+
* The PUT method replaces all current representations of the target resource with the request payload.
645+
*/
646+
PUT = "PUT",
647+
/**
648+
* The HEAD method asks for a response identical to that of a GET request, but without the response body.
649+
*/
650+
HEAD = "HEAD",
651+
/**
652+
* 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.
653+
*/
654+
POST = "POST",
655+
/**
656+
* The DELETE method deletes the specified resource.
657+
*/
658+
DELETE = "DELETE",
659+
}
660+
661+
/**
662+
* Specifies a cross-origin access rule for an Amazon S3 bucket.
663+
*/
664+
export interface CorsRule {
665+
/**
666+
* A unique identifier for this rule.
667+
*
668+
* @default - No id specified.
669+
*/
670+
readonly id?: string;
671+
/**
672+
* The time in seconds that your browser is to cache the preflight response for the specified resource.
673+
*
674+
* @default - No caching.
675+
*/
676+
readonly maxAge?: number;
677+
/**
678+
* Headers that are specified in the Access-Control-Request-Headers header.
679+
*
680+
* @default - No headers allowed.
681+
*/
682+
readonly allowedHeaders?: string[];
683+
/**
684+
* An HTTP method that you allow the origin to execute.
685+
*/
686+
readonly allowedMethods: HttpMethods[];
687+
/**
688+
* One or more origins you want customers to be able to access the bucket from.
689+
*/
690+
readonly allowedOrigins: string[];
691+
/**
692+
* One or more headers in the response that you want customers to be able to access from their applications.
693+
*
694+
* @default - No headers exposed.
695+
*/
696+
readonly exposedHeaders?: string[];
697+
}
698+
635699
export interface BucketProps {
636700
/**
637701
* The kind of server-side encryption to apply to this bucket.
@@ -725,6 +789,15 @@ export interface BucketProps {
725789
* @default - No metrics configuration.
726790
*/
727791
readonly metrics?: BucketMetrics[];
792+
793+
/**
794+
* The CORS configuration of this bucket.
795+
*
796+
* @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-s3-bucket-cors.html
797+
*
798+
* @default - No CORS configuration.
799+
*/
800+
readonly cors?: CorsRule[];
728801
}
729802

730803
/**
@@ -808,6 +881,7 @@ export class Bucket extends BucketBase {
808881
private readonly versioned?: boolean;
809882
private readonly notifications: BucketNotifications;
810883
private readonly metrics: BucketMetrics[] = [];
884+
private readonly cors: CorsRule[] = [];
811885

812886
constructor(scope: Construct, id: string, props: BucketProps = {}) {
813887
super(scope, id, {
@@ -826,7 +900,8 @@ export class Bucket extends BucketBase {
826900
lifecycleConfiguration: Lazy.anyValue({ produce: () => this.parseLifecycleConfiguration() }),
827901
websiteConfiguration: this.renderWebsiteConfiguration(props),
828902
publicAccessBlockConfiguration: props.blockPublicAccess,
829-
metricsConfigurations: Lazy.anyValue({ produce: () => this.parseMetricConfiguration() })
903+
metricsConfigurations: Lazy.anyValue({ produce: () => this.parseMetricConfiguration() }),
904+
corsConfiguration: Lazy.anyValue({ produce: () => this.parseCorsConfiguration() })
830905
});
831906

832907
applyRemovalPolicy(resource, props.removalPolicy !== undefined ? props.removalPolicy : RemovalPolicy.Orphan);
@@ -855,6 +930,8 @@ export class Bucket extends BucketBase {
855930

856931
// Add all bucket metric configurations rules
857932
(props.metrics || []).forEach(this.addMetric.bind(this));
933+
// Add all cors configuration rules
934+
(props.cors || []).forEach(this.addCorsRule.bind(this));
858935

859936
// Add all lifecycle rules
860937
(props.lifecycleRules || []).forEach(this.addLifecycleRule.bind(this));
@@ -892,6 +969,15 @@ export class Bucket extends BucketBase {
892969
this.metrics.push(metric);
893970
}
894971

972+
/**
973+
* Adds a cross-origin access configuration for objects in an Amazon S3 bucket
974+
*
975+
* @param rule The CORS configuration rule to add
976+
*/
977+
public addCorsRule(rule: CorsRule) {
978+
this.cors.push(rule);
979+
}
980+
895981
/**
896982
* Adds a bucket notification event destination.
897983
* @param event The event to trigger the notification
@@ -1103,6 +1189,25 @@ export class Bucket extends BucketBase {
11031189
}
11041190
}
11051191

1192+
private parseCorsConfiguration(): CfnBucket.CorsConfigurationProperty | undefined {
1193+
if (!this.cors || this.cors.length === 0) {
1194+
return undefined;
1195+
}
1196+
1197+
return { corsRules: this.cors.map(parseCors) };
1198+
1199+
function parseCors(rule: CorsRule): CfnBucket.CorsRuleProperty {
1200+
return {
1201+
id: rule.id,
1202+
maxAge: rule.maxAge,
1203+
allowedHeaders: rule.allowedHeaders,
1204+
allowedMethods: rule.allowedMethods,
1205+
allowedOrigins: rule.allowedOrigins,
1206+
exposedHeaders: rule.exposedHeaders
1207+
};
1208+
}
1209+
}
1210+
11061211
private parseTagFilters(tagFilters?: {[tag: string]: any}) {
11071212
if (!tagFilters || tagFilters.length === 0) {
11081213
return undefined;
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import { expect, haveResource } from '@aws-cdk/assert';
2+
import { Stack } from '@aws-cdk/cdk';
3+
import { Test } from 'nodeunit';
4+
import { Bucket, HttpMethods } from '../lib';
5+
6+
export = {
7+
'Can use addCors() to add a CORS configuration'(test: Test) {
8+
// GIVEN
9+
const stack = new Stack();
10+
11+
// WHEN
12+
const bucket = new Bucket(stack, 'Bucket');
13+
bucket.addCorsRule({
14+
allowedMethods: [HttpMethods.GET, HttpMethods.HEAD],
15+
allowedOrigins: ["https://example.com"]
16+
});
17+
18+
// THEN
19+
expect(stack).to(haveResource('AWS::S3::Bucket', {
20+
CorsConfiguration: {
21+
CorsRules: [{
22+
AllowedMethods: ["GET", "HEAD"],
23+
AllowedOrigins: ["https://example.com"]
24+
}]
25+
}
26+
}));
27+
28+
test.done();
29+
},
30+
31+
'Bucket with multiple cors configurations'(test: Test) {
32+
// GIVEN
33+
const stack = new Stack();
34+
35+
// WHEN
36+
new Bucket(stack, 'Bucket', {
37+
cors: [
38+
{
39+
allowedHeaders: [
40+
"*"
41+
],
42+
allowedMethods: [
43+
HttpMethods.GET
44+
],
45+
allowedOrigins: [
46+
"*"
47+
],
48+
exposedHeaders: [
49+
"Date"
50+
],
51+
id: "myCORSRuleId1",
52+
maxAge: 3600
53+
},
54+
{
55+
allowedHeaders: [
56+
"x-amz-*"
57+
],
58+
allowedMethods: [
59+
HttpMethods.DELETE
60+
],
61+
allowedOrigins: [
62+
"http://www.example1.com",
63+
"http://www.example2.com"
64+
],
65+
exposedHeaders: [
66+
"Connection",
67+
"Server",
68+
"Date"
69+
],
70+
id: "myCORSRuleId2",
71+
maxAge: 1800
72+
}
73+
]
74+
});
75+
76+
// THEN
77+
expect(stack).to(haveResource('AWS::S3::Bucket', {
78+
CorsConfiguration: {
79+
CorsRules: [
80+
{
81+
AllowedHeaders: [
82+
"*"
83+
],
84+
AllowedMethods: [
85+
"GET"
86+
],
87+
AllowedOrigins: [
88+
"*"
89+
],
90+
ExposedHeaders: [
91+
"Date"
92+
],
93+
Id: "myCORSRuleId1",
94+
MaxAge: 3600
95+
},
96+
{
97+
AllowedHeaders: [
98+
"x-amz-*"
99+
],
100+
AllowedMethods: [
101+
"DELETE"
102+
],
103+
AllowedOrigins: [
104+
"http://www.example1.com",
105+
"http://www.example2.com"
106+
],
107+
ExposedHeaders: [
108+
"Connection",
109+
"Server",
110+
"Date"
111+
],
112+
Id: "myCORSRuleId2",
113+
MaxAge: 1800
114+
}
115+
]
116+
}
117+
}));
118+
119+
test.done();
120+
},
121+
};

0 commit comments

Comments
 (0)