From 33f3554d8169c489226938d7ae94668c30f8dcf3 Mon Sep 17 00:00:00 2001 From: Jimmy Gaussen Date: Thu, 8 Aug 2019 10:13:07 +0200 Subject: [PATCH] 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 --- packages/@aws-cdk/aws-s3/README.md | 36 ++++++ packages/@aws-cdk/aws-s3/lib/bucket.ts | 116 ++++++++++++++++++- packages/@aws-cdk/aws-s3/test/test.bucket.ts | 60 +++++++++- 3 files changed, 206 insertions(+), 6 deletions(-) diff --git a/packages/@aws-cdk/aws-s3/README.md b/packages/@aws-cdk/aws-s3/README.md index d8581fcc10574..5296acee5d948 100644 --- a/packages/@aws-cdk/aws-s3/README.md +++ b/packages/@aws-cdk/aws-s3/README.md @@ -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', + } + }] +}); +``` diff --git a/packages/@aws-cdk/aws-s3/lib/bucket.ts b/packages/@aws-cdk/aws-s3/lib/bucket.ts index ad09adace9bad..98d14fe55b46a 100644 --- a/packages/@aws-cdk/aws-s3/lib/bucket.ts +++ b/packages/@aws-cdk/aws-s3/lib/bucket.ts @@ -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. * @@ -1256,7 +1263,7 @@ 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; } @@ -1264,14 +1271,32 @@ export class Bucket extends BucketBase { 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((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 }; } } @@ -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(list: T[] | undefined, callback: (element: T) => U): U[] | undefined { if (!list || list.length === 0) { return undefined; diff --git a/packages/@aws-cdk/aws-s3/test/test.bucket.ts b/packages/@aws-cdk/aws-s3/test/test.bucket.ts index 71e7a7c1b763c..ea12c6777bfa9 100644 --- a/packages/@aws-cdk/aws-s3/test/test.bucket.ts +++ b/packages/@aws-cdk/aws-s3/test/test.bucket.ts @@ -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', { @@ -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(); }, },