diff --git a/packages/@aws-cdk/aws-s3/README.md b/packages/@aws-cdk/aws-s3/README.md index 3de0245cdb76e..63d581b63ea73 100644 --- a/packages/@aws-cdk/aws-s3/README.md +++ b/packages/@aws-cdk/aws-s3/README.md @@ -208,3 +208,33 @@ bucket.onEvent(s3.EventType.ObjectRemoved, myQueue, { prefix: 'foo/', suffix: '. ``` [S3 Bucket Notifications]: https://docs.aws.amazon.com/AmazonS3/latest/dev/NotificationHowTo.html + + +### Block Public Access + +Use `blockPublicAccess` to specify [block public access settings] on the bucket. + +Enable all block public access settings: +```ts +const bucket = new Bucket(this, 'MyBlockedBucket', { + blockPublicAccess: BlockPublicAccess.BlockAll +}); +``` + +Block and ignore public ACLs: +```ts +const bucket = new Bucket(this, 'MyBlockedBucket', { + blockPublicAccess: BlockPublicAccess.BlockAcls +}); +``` + +Alternatively, specify the settings manually: +```ts +const bucket = new Bucket(this, 'MyBlockedBucket', { + blockPublicAccess: new BlockPublicAccess({ blockPublicPolicy: true }) +}); +``` + +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 diff --git a/packages/@aws-cdk/aws-s3/lib/bucket.ts b/packages/@aws-cdk/aws-s3/lib/bucket.ts index e10dedb078c2f..df76d4520044f 100644 --- a/packages/@aws-cdk/aws-s3/lib/bucket.ts +++ b/packages/@aws-cdk/aws-s3/lib/bucket.ts @@ -293,6 +293,11 @@ export abstract class BucketBase extends cdk.Construct implements IBucket { */ protected abstract autoCreatePolicy = false; + /** + * Whether to disallow public access + */ + protected abstract disallowPublicAccess?: boolean; + /** * Exports this bucket from the stack. */ @@ -514,6 +519,10 @@ export abstract class BucketBase extends cdk.Construct implements IBucket { * @returns The `iam.PolicyStatement` object, which can be used to apply e.g. conditions. */ public grantPublicAccess(keyPrefix = '*', ...allowedActions: string[]): iam.PolicyStatement { + if (this.disallowPublicAccess) { + throw new Error("Cannot grant public access when 'blockPublicPolicy' is enabled"); + } + allowedActions = allowedActions.length > 0 ? allowedActions : [ 's3:GetObject' ]; const statement = new iam.PolicyStatement() @@ -555,6 +564,62 @@ export abstract class BucketBase extends cdk.Construct implements IBucket { } } +export interface BlockPublicAccessOptions { + /** + * Whether to block public ACLs + * + * @see https://docs.aws.amazon.com/AmazonS3/latest/dev/access-control-block-public-access.html#access-control-block-public-access-options + */ + blockPublicAcls?: boolean; + + /** + * Whether to block public policy + * + * @see https://docs.aws.amazon.com/AmazonS3/latest/dev/access-control-block-public-access.html#access-control-block-public-access-options + */ + blockPublicPolicy?: boolean; + + /** + * Whether to ignore public ACLs + * + * @see https://docs.aws.amazon.com/AmazonS3/latest/dev/access-control-block-public-access.html#access-control-block-public-access-options + */ + ignorePublicAcls?: boolean; + + /** + * Whether to restrict public access + * + * @see https://docs.aws.amazon.com/AmazonS3/latest/dev/access-control-block-public-access.html#access-control-block-public-access-options + */ + restrictPublicBuckets?: boolean; +} + +export class BlockPublicAccess { + public static readonly BlockAll = new BlockPublicAccess({ + blockPublicAcls: true, + blockPublicPolicy: true, + ignorePublicAcls: true, + restrictPublicBuckets: true + }); + + public static readonly BlockAcls = new BlockPublicAccess({ + blockPublicAcls: true, + ignorePublicAcls: true + }); + + public blockPublicAcls: boolean | undefined; + public blockPublicPolicy: boolean | undefined; + public ignorePublicAcls: boolean | undefined; + public restrictPublicBuckets: boolean | undefined; + + constructor(options: BlockPublicAccessOptions) { + this.blockPublicAcls = options.blockPublicAcls; + this.blockPublicPolicy = options.blockPublicPolicy; + this.ignorePublicAcls = options.ignorePublicAcls; + this.restrictPublicBuckets = options.restrictPublicBuckets; + } +} + export interface BucketProps { /** * The kind of server-side encryption to apply to this bucket. @@ -623,6 +688,13 @@ export interface BucketProps { * Similar to calling `bucket.grantPublicAccess()` */ publicReadAccess?: boolean; + + /** + * The block public access configuration of this bucket. + * + * @see https://docs.aws.amazon.com/AmazonS3/latest/dev/access-control-block-public-access.html + */ + blockPublicAccess?: BlockPublicAccess; } /** @@ -652,6 +724,7 @@ export class Bucket extends BucketBase { public readonly encryptionKey?: kms.IEncryptionKey; public policy?: BucketPolicy; protected autoCreatePolicy = true; + protected disallowPublicAccess?: boolean; private readonly lifecycleRules: LifecycleRule[] = []; private readonly versioned?: boolean; private readonly notifications: BucketNotifications; @@ -666,7 +739,8 @@ export class Bucket extends BucketBase { bucketEncryption, versioningConfiguration: props.versioned ? { status: 'Enabled' } : undefined, lifecycleConfiguration: new cdk.Token(() => this.parseLifecycleConfiguration()), - websiteConfiguration: this.renderWebsiteConfiguration(props) + websiteConfiguration: this.renderWebsiteConfiguration(props), + publicAccessBlockConfiguration: props.blockPublicAccess }); cdk.applyRemovalPolicy(resource, props.removalPolicy !== undefined ? props.removalPolicy : cdk.RemovalPolicy.Orphan); @@ -678,6 +752,7 @@ export class Bucket extends BucketBase { this.domainName = resource.bucketDomainName; this.bucketWebsiteUrl = resource.bucketWebsiteUrl; this.dualstackDomainName = resource.bucketDualStackDomainName; + this.disallowPublicAccess = props.blockPublicAccess && props.blockPublicAccess.blockPublicPolicy; // Add all lifecycle rules (props.lifecycleRules || []).forEach(this.addLifecycleRule.bind(this)); @@ -1042,6 +1117,8 @@ class ImportedBucket extends BucketBase { public policy?: BucketPolicy; protected autoCreatePolicy: boolean; + protected disallowPublicAccess?: boolean; + constructor(scope: cdk.Construct, id: string, private readonly props: BucketImportProps) { super(scope, id); @@ -1059,6 +1136,7 @@ class ImportedBucket extends BucketBase { ? false : props.bucketWebsiteNewUrlFormat; this.policy = undefined; + this.disallowPublicAccess = false; } /** diff --git a/packages/@aws-cdk/aws-s3/test/test.bucket.ts b/packages/@aws-cdk/aws-s3/test/test.bucket.ts index 19c10b0840951..e9363faddfdee 100644 --- a/packages/@aws-cdk/aws-s3/test/test.bucket.ts +++ b/packages/@aws-cdk/aws-s3/test/test.bucket.ts @@ -201,6 +201,76 @@ export = { test.done(); }, + 'bucket with block public access set to BlockAll'(test: Test) { + const stack = new cdk.Stack(); + new s3.Bucket(stack, 'MyBucket', { + blockPublicAccess: s3.BlockPublicAccess.BlockAll, + }); + + expect(stack).toMatch({ + "Resources": { + "MyBucketF68F3FF0": { + "Type": "AWS::S3::Bucket", + "Properties": { + "PublicAccessBlockConfiguration": { + "BlockPublicAcls": true, + "BlockPublicPolicy": true, + "IgnorePublicAcls": true, + "RestrictPublicBuckets": true, + } + }, + "DeletionPolicy": "Retain", + } + } + }); + test.done(); + }, + + 'bucket with block public access set to BlockAcls'(test: Test) { + const stack = new cdk.Stack(); + new s3.Bucket(stack, 'MyBucket', { + blockPublicAccess: s3.BlockPublicAccess.BlockAcls, + }); + + expect(stack).toMatch({ + "Resources": { + "MyBucketF68F3FF0": { + "Type": "AWS::S3::Bucket", + "Properties": { + "PublicAccessBlockConfiguration": { + "BlockPublicAcls": true, + "IgnorePublicAcls": true, + } + }, + "DeletionPolicy": "Retain", + } + } + }); + test.done(); + }, + + 'bucket with custom block public access setting'(test: Test) { + const stack = new cdk.Stack(); + new s3.Bucket(stack, 'MyBucket', { + blockPublicAccess: new s3.BlockPublicAccess({ restrictPublicBuckets: true }) + }); + + expect(stack).toMatch({ + "Resources": { + "MyBucketF68F3FF0": { + "Type": "AWS::S3::Bucket", + "Properties": { + "PublicAccessBlockConfiguration": { + "RestrictPublicBuckets": true, + } + }, + "DeletionPolicy": "Retain", + } + } + }); + test.done(); + }, + 'permissions': { 'addPermission creates a bucket policy'(test: Test) { @@ -1175,6 +1245,19 @@ export = { "Version": "2012-10-17" } })); + test.done(); + }, + + 'throws when blockPublicPolicy is set to true'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const bucket = new s3.Bucket(stack, 'MyBucket', { + blockPublicAccess: new s3.BlockPublicAccess({ blockPublicPolicy: true }) + }); + + // THEN + test.throws(() => bucket.grantPublicAccess(), /blockPublicPolicy/); + test.done(); } },