Skip to content

Commit

Permalink
feat(s3): website routing rules (#3411)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
nmussy authored and mergify[bot] committed Aug 8, 2019
1 parent 5d4a275 commit 33f3554
Show file tree
Hide file tree
Showing 3 changed files with 206 additions and 6 deletions.
36 changes: 36 additions & 0 deletions packages/@aws-cdk/aws-s3/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,3 +191,39 @@ const bucket = new Bucket(this, 'MyBlockedBucket', {
When `blockPublicPolicy` is set to `true`, `grantPublicRead()` throws an error.

[block public access settings]: https://docs.aws.amazon.com/AmazonS3/latest/dev/access-control-block-public-access.html


### Website redirection

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.

[redirection policy]: https://docs.aws.amazon.com/AmazonS3/latest/dev/how-to-page-redirect.html#advanced-conditional-redirects

#### Static redirection

You can statically redirect a to a given Bucket URL or any other host name with `websiteRedirect`:

```ts
const bucket = new Bucket(this, 'MyRedirectedBucket', {
websiteRedirect: { hostName: 'www.example.com' }
});
```

#### Routing rules

Alternatively, you can also define multiple `websiteRoutingRules`, to define complex, conditional redirections:

```ts
const bucket = new Bucket(this, 'MyRedirectedBucket', {
websiteRoutingRules: [{
hostName: 'www.example.com',
httpRedirectCode: '302',
protocol: RedirectProtocol.HTTPS,
replaceKey: ReplaceKey.prefixWith('test/'),
condition: {
httpErrorCodeReturnedEquals: '200',
keyPrefixEquals: 'prefix',
}
}]
});
```
116 changes: 112 additions & 4 deletions packages/@aws-cdk/aws-s3/lib/bucket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -790,12 +790,19 @@ export interface BucketProps {
/**
* Specifies the redirect behavior of all requests to a website endpoint of a bucket.
*
* If you specify this property, you can't specify "websiteIndexDocument" nor "websiteErrorDocument".
* If you specify this property, you can't specify "websiteIndexDocument", "websiteErrorDocument" nor , "websiteRoutingRules".
*
* @default - No redirection.
*/
readonly websiteRedirect?: RedirectTarget;

/**
* Rules that define when a redirect is applied and the redirect behavior
*
* @default - No redirection rules.
*/
readonly websiteRoutingRules?: RoutingRule[];

/**
* Specifies a canned ACL that grants predefined permissions to the bucket.
*
Expand Down Expand Up @@ -1256,22 +1263,40 @@ export class Bucket extends BucketBase {
}

private renderWebsiteConfiguration(props: BucketProps): CfnBucket.WebsiteConfigurationProperty | undefined {
if (!props.websiteErrorDocument && !props.websiteIndexDocument && !props.websiteRedirect) {
if (!props.websiteErrorDocument && !props.websiteIndexDocument && !props.websiteRedirect && !props.websiteRoutingRules) {
return undefined;
}

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

if (props.websiteRedirect && (props.websiteErrorDocument || props.websiteIndexDocument)) {
throw new Error('"websiteIndexDocument" and "websiteErrorDocument" cannot be set if "websiteRedirect" is used');
if (props.websiteRedirect && (props.websiteErrorDocument || props.websiteIndexDocument || props.websiteRoutingRules)) {
throw new Error('"websiteIndexDocument", "websiteErrorDocument" and, "websiteRoutingRules" cannot be set if "websiteRedirect" is used');
}

const routingRules = props.websiteRoutingRules ? props.websiteRoutingRules.map<CfnBucket.RoutingRuleProperty>((rule) => {
if (rule.condition && !rule.condition.httpErrorCodeReturnedEquals && !rule.condition.keyPrefixEquals) {
throw new Error('The condition property cannot be an empty object');
}

return {
redirectRule: {
hostName: rule.hostName,
httpRedirectCode: rule.httpRedirectCode,
protocol: rule.protocol,
replaceKeyWith: rule.replaceKey && rule.replaceKey.withKey,
replaceKeyPrefixWith: rule.replaceKey && rule.replaceKey.prefixWithKey,
},
routingRuleCondition: rule.condition
};
}) : undefined;

return {
indexDocument: props.websiteIndexDocument,
errorDocument: props.websiteErrorDocument,
redirectAllRequestsTo: props.websiteRedirect,
routingRules
};
}
}
Expand Down Expand Up @@ -1485,6 +1510,89 @@ export enum BucketAccessControl {
AWS_EXEC_READ = 'AwsExecRead',
}

export interface RoutingRuleCondition {
/**
* The HTTP error code when the redirect is applied
*
* In the event of an error, if the error code equals this value, then the specified redirect is applied.
*
* If both condition properties are specified, both must be true for the redirect to be applied.
*
* @default - The HTTP error code will not be verified
*/
readonly httpErrorCodeReturnedEquals?: string;

/**
* The object key name prefix when the redirect is applied
*
* If both condition properties are specified, both must be true for the redirect to be applied.
*
* @default - The object key name will not be verified
*/
readonly keyPrefixEquals?: string;
}

export class ReplaceKey {
/**
* The specific object key to use in the redirect request
*/
public static with(keyReplacement: string) {
return new this(keyReplacement);
}

/**
* The object key prefix to use in the redirect request
*/
public static prefixWith(keyReplacement: string) {
return new this(undefined, keyReplacement);
}

private constructor(public readonly withKey?: string, public readonly prefixWithKey?: string) {
}
}

/**
* Rule that define when a redirect is applied and the redirect behavior.
*
* @see https://docs.aws.amazon.com/AmazonS3/latest/dev/how-to-page-redirect.html
*/
export interface RoutingRule {
/**
* The host name to use in the redirect request
*
* @default - The host name used in the original request.
*/
readonly hostName?: string;

/**
* The HTTP redirect code to use on the response
*
* @default "301" - Moved Permanently
*/
readonly httpRedirectCode?: string;

/**
* Protocol to use when redirecting requests
*
* @default - The protocol used in the original request.
*/
readonly protocol?: RedirectProtocol;

/**
* Specifies the object key prefix to use in the redirect request
*
* @default - The key will not be replaced
*/
readonly replaceKey?: ReplaceKey;

/**
* Specifies a condition that must be met for the specified redirect to apply.
*
* @default - No condition
*/
readonly condition?: RoutingRuleCondition;
}

function mapOrUndefined<T, U>(list: T[] | undefined, callback: (element: T) => U): U[] | undefined {
if (!list || list.length === 0) {
return undefined;
Expand Down
60 changes: 58 additions & 2 deletions packages/@aws-cdk/aws-s3/test/test.bucket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1566,7 +1566,7 @@ export = {
}));
test.done();
},
'fails if websiteRedirect and another website property are specified'(test: Test) {
'fails if websiteRedirect and websiteIndex and websiteError are specified'(test: Test) {
const stack = new cdk.Stack();
test.throws(() => {
new s3.Bucket(stack, 'Website', {
Expand All @@ -1576,7 +1576,63 @@ export = {
hostName: 'www.example.com'
}
});
}, /"websiteIndexDocument" and "websiteErrorDocument" cannot be set if "websiteRedirect" is used/);
}, /"websiteIndexDocument", "websiteErrorDocument" and, "websiteRoutingRules" cannot be set if "websiteRedirect" is used/);
test.done();
},
'fails if websiteRedirect and websiteRoutingRules are specified'(test: Test) {
const stack = new cdk.Stack();
test.throws(() => {
new s3.Bucket(stack, 'Website', {
websiteRoutingRules: [],
websiteRedirect: {
hostName: 'www.example.com'
}
});
}, /"websiteIndexDocument", "websiteErrorDocument" and, "websiteRoutingRules" cannot be set if "websiteRedirect" is used/);
test.done();
},
'adds RedirectRules property'(test: Test) {
const stack = new cdk.Stack();
new s3.Bucket(stack, 'Website', {
websiteRoutingRules: [{
hostName: 'www.example.com',
httpRedirectCode: '302',
protocol: s3.RedirectProtocol.HTTPS,
replaceKey: s3.ReplaceKey.prefixWith('test/'),
condition: {
httpErrorCodeReturnedEquals: '200',
keyPrefixEquals: 'prefix',
}
}]
});
expect(stack).to(haveResource('AWS::S3::Bucket', {
WebsiteConfiguration: {
RoutingRules: [{
RedirectRule: {
HostName: 'www.example.com',
HttpRedirectCode: '302',
Protocol: 'https',
ReplaceKeyPrefixWith: 'test/'
},
RoutingRuleCondition: {
HttpErrorCodeReturnedEquals: '200',
KeyPrefixEquals: 'prefix'
}
}]
}
}));
test.done();
},
'fails if routingRule condition object is empty'(test: Test) {
const stack = new cdk.Stack();
test.throws(() => {
new s3.Bucket(stack, 'Website', {
websiteRoutingRules: [{
httpRedirectCode: '303',
condition: {}
}]
});
}, /The condition property cannot be an empty object/);
test.done();
},
},
Expand Down

0 comments on commit 33f3554

Please sign in to comment.