Skip to content

Commit 33f3554

Browse files
nmussymergify[bot]
authored andcommitted
feat(s3): website routing rules (#3411)
* feat(s3): websiteRoutingRules property * fix(s3): update @default RoutingRuleProps * fix(s3): JSDoc cleanup * fix(s3): throw if routingRule is invalid * fix(s3): remove incorrect exception * fix(s3): remove shadowed variable * fix(s3): remove "not required siblings" from JSDoc * fix(s3): refactor RoutingRule class into object * fix(s3): refactor replaceKey union interface into class * chore(s3): document websiteRedirect and websiteRoutingRules
1 parent 5d4a275 commit 33f3554

File tree

3 files changed

+206
-6
lines changed

3 files changed

+206
-6
lines changed

packages/@aws-cdk/aws-s3/README.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,3 +191,39 @@ const bucket = new Bucket(this, 'MyBlockedBucket', {
191191
When `blockPublicPolicy` is set to `true`, `grantPublicRead()` throws an error.
192192

193193
[block public access settings]: https://docs.aws.amazon.com/AmazonS3/latest/dev/access-control-block-public-access.html
194+
195+
196+
### Website redirection
197+
198+
You can use the two following properties to specify the bucket [redirection policy]. Please note that these methods cannot both be applied to the same bucket.
199+
200+
[redirection policy]: https://docs.aws.amazon.com/AmazonS3/latest/dev/how-to-page-redirect.html#advanced-conditional-redirects
201+
202+
#### Static redirection
203+
204+
You can statically redirect a to a given Bucket URL or any other host name with `websiteRedirect`:
205+
206+
```ts
207+
const bucket = new Bucket(this, 'MyRedirectedBucket', {
208+
websiteRedirect: { hostName: 'www.example.com' }
209+
});
210+
```
211+
212+
#### Routing rules
213+
214+
Alternatively, you can also define multiple `websiteRoutingRules`, to define complex, conditional redirections:
215+
216+
```ts
217+
const bucket = new Bucket(this, 'MyRedirectedBucket', {
218+
websiteRoutingRules: [{
219+
hostName: 'www.example.com',
220+
httpRedirectCode: '302',
221+
protocol: RedirectProtocol.HTTPS,
222+
replaceKey: ReplaceKey.prefixWith('test/'),
223+
condition: {
224+
httpErrorCodeReturnedEquals: '200',
225+
keyPrefixEquals: 'prefix',
226+
}
227+
}]
228+
});
229+
```

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

Lines changed: 112 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -790,12 +790,19 @@ export interface BucketProps {
790790
/**
791791
* Specifies the redirect behavior of all requests to a website endpoint of a bucket.
792792
*
793-
* If you specify this property, you can't specify "websiteIndexDocument" nor "websiteErrorDocument".
793+
* If you specify this property, you can't specify "websiteIndexDocument", "websiteErrorDocument" nor , "websiteRoutingRules".
794794
*
795795
* @default - No redirection.
796796
*/
797797
readonly websiteRedirect?: RedirectTarget;
798798

799+
/**
800+
* Rules that define when a redirect is applied and the redirect behavior
801+
*
802+
* @default - No redirection rules.
803+
*/
804+
readonly websiteRoutingRules?: RoutingRule[];
805+
799806
/**
800807
* Specifies a canned ACL that grants predefined permissions to the bucket.
801808
*
@@ -1256,22 +1263,40 @@ export class Bucket extends BucketBase {
12561263
}
12571264

12581265
private renderWebsiteConfiguration(props: BucketProps): CfnBucket.WebsiteConfigurationProperty | undefined {
1259-
if (!props.websiteErrorDocument && !props.websiteIndexDocument && !props.websiteRedirect) {
1266+
if (!props.websiteErrorDocument && !props.websiteIndexDocument && !props.websiteRedirect && !props.websiteRoutingRules) {
12601267
return undefined;
12611268
}
12621269

12631270
if (props.websiteErrorDocument && !props.websiteIndexDocument) {
12641271
throw new Error(`"websiteIndexDocument" is required if "websiteErrorDocument" is set`);
12651272
}
12661273

1267-
if (props.websiteRedirect && (props.websiteErrorDocument || props.websiteIndexDocument)) {
1268-
throw new Error('"websiteIndexDocument" and "websiteErrorDocument" cannot be set if "websiteRedirect" is used');
1274+
if (props.websiteRedirect && (props.websiteErrorDocument || props.websiteIndexDocument || props.websiteRoutingRules)) {
1275+
throw new Error('"websiteIndexDocument", "websiteErrorDocument" and, "websiteRoutingRules" cannot be set if "websiteRedirect" is used');
12691276
}
12701277

1278+
const routingRules = props.websiteRoutingRules ? props.websiteRoutingRules.map<CfnBucket.RoutingRuleProperty>((rule) => {
1279+
if (rule.condition && !rule.condition.httpErrorCodeReturnedEquals && !rule.condition.keyPrefixEquals) {
1280+
throw new Error('The condition property cannot be an empty object');
1281+
}
1282+
1283+
return {
1284+
redirectRule: {
1285+
hostName: rule.hostName,
1286+
httpRedirectCode: rule.httpRedirectCode,
1287+
protocol: rule.protocol,
1288+
replaceKeyWith: rule.replaceKey && rule.replaceKey.withKey,
1289+
replaceKeyPrefixWith: rule.replaceKey && rule.replaceKey.prefixWithKey,
1290+
},
1291+
routingRuleCondition: rule.condition
1292+
};
1293+
}) : undefined;
1294+
12711295
return {
12721296
indexDocument: props.websiteIndexDocument,
12731297
errorDocument: props.websiteErrorDocument,
12741298
redirectAllRequestsTo: props.websiteRedirect,
1299+
routingRules
12751300
};
12761301
}
12771302
}
@@ -1485,6 +1510,89 @@ export enum BucketAccessControl {
14851510
AWS_EXEC_READ = 'AwsExecRead',
14861511
}
14871512

1513+
export interface RoutingRuleCondition {
1514+
/**
1515+
* The HTTP error code when the redirect is applied
1516+
*
1517+
* In the event of an error, if the error code equals this value, then the specified redirect is applied.
1518+
*
1519+
* If both condition properties are specified, both must be true for the redirect to be applied.
1520+
*
1521+
* @default - The HTTP error code will not be verified
1522+
*/
1523+
readonly httpErrorCodeReturnedEquals?: string;
1524+
1525+
/**
1526+
* The object key name prefix when the redirect is applied
1527+
*
1528+
* If both condition properties are specified, both must be true for the redirect to be applied.
1529+
*
1530+
* @default - The object key name will not be verified
1531+
*/
1532+
readonly keyPrefixEquals?: string;
1533+
}
1534+
1535+
export class ReplaceKey {
1536+
/**
1537+
* The specific object key to use in the redirect request
1538+
*/
1539+
public static with(keyReplacement: string) {
1540+
return new this(keyReplacement);
1541+
}
1542+
1543+
/**
1544+
* The object key prefix to use in the redirect request
1545+
*/
1546+
public static prefixWith(keyReplacement: string) {
1547+
return new this(undefined, keyReplacement);
1548+
}
1549+
1550+
private constructor(public readonly withKey?: string, public readonly prefixWithKey?: string) {
1551+
}
1552+
}
1553+
1554+
/**
1555+
* Rule that define when a redirect is applied and the redirect behavior.
1556+
*
1557+
* @see https://docs.aws.amazon.com/AmazonS3/latest/dev/how-to-page-redirect.html
1558+
*/
1559+
export interface RoutingRule {
1560+
/**
1561+
* The host name to use in the redirect request
1562+
*
1563+
* @default - The host name used in the original request.
1564+
*/
1565+
readonly hostName?: string;
1566+
1567+
/**
1568+
* The HTTP redirect code to use on the response
1569+
*
1570+
* @default "301" - Moved Permanently
1571+
*/
1572+
readonly httpRedirectCode?: string;
1573+
1574+
/**
1575+
* Protocol to use when redirecting requests
1576+
*
1577+
* @default - The protocol used in the original request.
1578+
*/
1579+
readonly protocol?: RedirectProtocol;
1580+
1581+
/**
1582+
* Specifies the object key prefix to use in the redirect request
1583+
*
1584+
* @default - The key will not be replaced
1585+
*/
1586+
readonly replaceKey?: ReplaceKey;
1587+
1588+
/**
1589+
* Specifies a condition that must be met for the specified redirect to apply.
1590+
*
1591+
* @default - No condition
1592+
*/
1593+
readonly condition?: RoutingRuleCondition;
1594+
}
1595+
14881596
function mapOrUndefined<T, U>(list: T[] | undefined, callback: (element: T) => U): U[] | undefined {
14891597
if (!list || list.length === 0) {
14901598
return undefined;

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

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1566,7 +1566,7 @@ export = {
15661566
}));
15671567
test.done();
15681568
},
1569-
'fails if websiteRedirect and another website property are specified'(test: Test) {
1569+
'fails if websiteRedirect and websiteIndex and websiteError are specified'(test: Test) {
15701570
const stack = new cdk.Stack();
15711571
test.throws(() => {
15721572
new s3.Bucket(stack, 'Website', {
@@ -1576,7 +1576,63 @@ export = {
15761576
hostName: 'www.example.com'
15771577
}
15781578
});
1579-
}, /"websiteIndexDocument" and "websiteErrorDocument" cannot be set if "websiteRedirect" is used/);
1579+
}, /"websiteIndexDocument", "websiteErrorDocument" and, "websiteRoutingRules" cannot be set if "websiteRedirect" is used/);
1580+
test.done();
1581+
},
1582+
'fails if websiteRedirect and websiteRoutingRules are specified'(test: Test) {
1583+
const stack = new cdk.Stack();
1584+
test.throws(() => {
1585+
new s3.Bucket(stack, 'Website', {
1586+
websiteRoutingRules: [],
1587+
websiteRedirect: {
1588+
hostName: 'www.example.com'
1589+
}
1590+
});
1591+
}, /"websiteIndexDocument", "websiteErrorDocument" and, "websiteRoutingRules" cannot be set if "websiteRedirect" is used/);
1592+
test.done();
1593+
},
1594+
'adds RedirectRules property'(test: Test) {
1595+
const stack = new cdk.Stack();
1596+
new s3.Bucket(stack, 'Website', {
1597+
websiteRoutingRules: [{
1598+
hostName: 'www.example.com',
1599+
httpRedirectCode: '302',
1600+
protocol: s3.RedirectProtocol.HTTPS,
1601+
replaceKey: s3.ReplaceKey.prefixWith('test/'),
1602+
condition: {
1603+
httpErrorCodeReturnedEquals: '200',
1604+
keyPrefixEquals: 'prefix',
1605+
}
1606+
}]
1607+
});
1608+
expect(stack).to(haveResource('AWS::S3::Bucket', {
1609+
WebsiteConfiguration: {
1610+
RoutingRules: [{
1611+
RedirectRule: {
1612+
HostName: 'www.example.com',
1613+
HttpRedirectCode: '302',
1614+
Protocol: 'https',
1615+
ReplaceKeyPrefixWith: 'test/'
1616+
},
1617+
RoutingRuleCondition: {
1618+
HttpErrorCodeReturnedEquals: '200',
1619+
KeyPrefixEquals: 'prefix'
1620+
}
1621+
}]
1622+
}
1623+
}));
1624+
test.done();
1625+
},
1626+
'fails if routingRule condition object is empty'(test: Test) {
1627+
const stack = new cdk.Stack();
1628+
test.throws(() => {
1629+
new s3.Bucket(stack, 'Website', {
1630+
websiteRoutingRules: [{
1631+
httpRedirectCode: '303',
1632+
condition: {}
1633+
}]
1634+
});
1635+
}, /The condition property cannot be an empty object/);
15801636
test.done();
15811637
},
15821638
},

0 commit comments

Comments
 (0)