From 42dcb2b8ed97a67c4f626c4a872949e7038bbd72 Mon Sep 17 00:00:00 2001 From: Elad Ben-Israel Date: Wed, 10 Oct 2018 10:08:46 +0300 Subject: [PATCH] feat(aws-s3): support granting public access to objects Adds `bucket.grantPublicAccess` with a bunch of useful capabilities. Fixes #877 --- packages/@aws-cdk/aws-s3/lib/bucket.ts | 35 ++++++ packages/@aws-cdk/aws-s3/test/test.bucket.ts | 111 ++++++++++++++++++- 2 files changed, 143 insertions(+), 3 deletions(-) diff --git a/packages/@aws-cdk/aws-s3/lib/bucket.ts b/packages/@aws-cdk/aws-s3/lib/bucket.ts index b2fd2d20e7a99..2f56be427017f 100644 --- a/packages/@aws-cdk/aws-s3/lib/bucket.ts +++ b/packages/@aws-cdk/aws-s3/lib/bucket.ts @@ -269,6 +269,41 @@ export abstract class BucketRef extends cdk.Construct { this.arnForObjects(objectsKeyPattern)); } + /** + * Allows unrestricted access to objects from this bucket. + * + * IMPORTANT: This permission allows anyone to perform actions on S3 objects + * in this bucket, which is useful for when you configure your bucket as a + * website and want everyone to be able to read objects in the bucket without + * needing to authenticate. + * + * Without arguments, this method will grant read ("s3:GetObject") access to + * all objects ("*") in the bucket. + * + * The method returns the `iam.PolicyStatement` object, which can then be modified + * as needed. For example, you can add a condition that will restrict access only + * to an IPv4 range like this: + * + * const statement = bucket.grantPublicAccess(); + * statement.addCondition('IpAddress', { "aws:SourceIp": "54.240.143.0/24" }); + * + * + * @param keyPrefix the prefix of S3 object keys (e.g. `home/*`). Default is "*". + * @param allowedActions the set of S3 actions to allow. Default is "s3:GetObject". + * @returns The `iam.PolicyStatement` object, which can be used to apply e.g. conditions. + */ + public grantPublicAccess(keyPrefix = '*', ...allowedActions: string[]): iam.PolicyStatement { + allowedActions = allowedActions.length > 0 ? allowedActions : [ 's3:GetObject' ]; + + const statement = new iam.PolicyStatement() + .addActions(...allowedActions) + .addResource(this.arnForObjects(keyPrefix)) + .addPrincipal(new iam.Anyone()); + + this.addToResourcePolicy(statement); + return statement; + } + private grant(identity: iam.IPrincipal | undefined, bucketActions: string[], keyActions: string[], diff --git a/packages/@aws-cdk/aws-s3/test/test.bucket.ts b/packages/@aws-cdk/aws-s3/test/test.bucket.ts index e3c3a44f886ed..0878f6e593485 100644 --- a/packages/@aws-cdk/aws-s3/test/test.bucket.ts +++ b/packages/@aws-cdk/aws-s3/test/test.bucket.ts @@ -1,10 +1,9 @@ -import { expect } from '@aws-cdk/assert'; +import { expect, haveResource } from '@aws-cdk/assert'; import iam = require('@aws-cdk/aws-iam'); import kms = require('@aws-cdk/aws-kms'); import cdk = require('@aws-cdk/cdk'); import { Test } from 'nodeunit'; import s3 = require('../lib'); -import { Bucket } from '../lib'; // to make it easy to copy & paste from output: // tslint:disable:object-literal-key-quotes @@ -963,7 +962,7 @@ export = { 'urlForObject returns a token with the S3 URL of the token'(test: Test) { const stack = new cdk.Stack(); - const bucket = new Bucket(stack, 'MyBucket'); + const bucket = new s3.Bucket(stack, 'MyBucket'); new cdk.Output(stack, 'BucketURL', { value: bucket.bucketUrl }); new cdk.Output(stack, 'MyFileURL', { value: bucket.urlForObject('my/file.txt') }); @@ -1059,5 +1058,111 @@ export = { }); test.done(); + }, + + 'grantPublicAccess': { + 'by default, grants s3:GetObject to all objects'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const bucket = new s3.Bucket(stack, 'b'); + + // WHEN + bucket.grantPublicAccess(); + + // THEN + expect(stack).to(haveResource('AWS::S3::BucketPolicy', { + "PolicyDocument": { + "Statement": [ + { + "Action": "s3:GetObject", + "Effect": "Allow", + "Principal": "*", + "Resource": { "Fn::Join": [ "", [ { "Fn::GetAtt": [ "bC3BBCC65", "Arn" ] }, "/", "*" ] ] } + } + ], + "Version": "2012-10-17" + } + })); + test.done(); + }, + + '"keyPrefix" can be used to only grant access to certain objects'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const bucket = new s3.Bucket(stack, 'b'); + + // WHEN + bucket.grantPublicAccess('only/access/these/*'); + + // THEN + expect(stack).to(haveResource('AWS::S3::BucketPolicy', { + "PolicyDocument": { + "Statement": [ + { + "Action": "s3:GetObject", + "Effect": "Allow", + "Principal": "*", + "Resource": { "Fn::Join": [ "", [ { "Fn::GetAtt": [ "bC3BBCC65", "Arn" ] }, "/", "only/access/these/*" ] ] } + } + ], + "Version": "2012-10-17" + } + })); + test.done(); + }, + + '"allowedActions" can be used to specify actions explicitly'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const bucket = new s3.Bucket(stack, 'b'); + + // WHEN + bucket.grantPublicAccess('*', 's3:GetObject', 's3:PutObject'); + + // THEN + expect(stack).to(haveResource('AWS::S3::BucketPolicy', { + "PolicyDocument": { + "Statement": [ + { + "Action": [ "s3:GetObject", "s3:PutObject" ], + "Effect": "Allow", + "Principal": "*", + "Resource": { "Fn::Join": [ "", [ { "Fn::GetAtt": [ "bC3BBCC65", "Arn" ] }, "/", "*" ] ] } + } + ], + "Version": "2012-10-17" + } + })); + test.done(); + }, + + 'returns the PolicyStatement which can be then customized'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const bucket = new s3.Bucket(stack, 'b'); + + // WHEN + const statement = bucket.grantPublicAccess(); + statement.addCondition('IpAddress', { "aws:SourceIp": "54.240.143.0/24" }); + + // THEN + expect(stack).to(haveResource('AWS::S3::BucketPolicy', { + "PolicyDocument": { + "Statement": [ + { + "Action": "s3:GetObject", + "Effect": "Allow", + "Principal": "*", + "Resource": { "Fn::Join": [ "", [ { "Fn::GetAtt": [ "bC3BBCC65", "Arn" ] }, "/", "*" ] ] }, + "Condition": { + "IpAddress": { "aws:SourceIp": "54.240.143.0/24" } + } + } + ], + "Version": "2012-10-17" + } + })); + test.done(); + } } };