Skip to content

Commit 7aaae57

Browse files
Add CloudFront AccessLevel.READ_VERSIONED
This allows creating an S3 bucket origin OriginAccessControl for access of versioned objects Fixes aws#33034
1 parent a928748 commit 7aaae57

File tree

5 files changed

+201
-119
lines changed

5 files changed

+201
-119
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import * as cloudfront from 'aws-cdk-lib/aws-cloudfront';
2+
import * as s3 from 'aws-cdk-lib/aws-s3';
3+
import * as cdk from 'aws-cdk-lib';
4+
import * as origins from 'aws-cdk-lib/aws-cloudfront-origins';
5+
import { ExpectedResult, IntegTest } from '@aws-cdk/integ-tests-alpha';
6+
7+
const app = new cdk.App();
8+
9+
const stack = new cdk.Stack(app, 'cloudfront-s3-bucket-origin-oac-read-versioned-access');
10+
11+
const bucket = new s3.Bucket(stack, 'Bucket', {
12+
removalPolicy: cdk.RemovalPolicy.DESTROY,
13+
});
14+
origins.S3BucketOrigin.withOriginAccessControl(bucket, {
15+
originAccessLevels: [cloudfront.AccessLevel.READ, cloudfront.AccessLevel.READ_VERSIONED],
16+
});
17+
18+
const integ = new IntegTest(app, 's3-origin-oac-read-versioned-access', {
19+
testCases: [stack],
20+
});
21+
22+
integ.assertions.awsApiCall('S3', 'getBucketPolicy', {
23+
Bucket: bucket.bucketName,
24+
}).expect(ExpectedResult.objectLike({ Statement: [{ Action: ['s3:GetObject', 's3:GetObjectVersion'] }] }));

packages/aws-cdk-lib/aws-cloudfront-origins/README.md

+5-3
Original file line numberDiff line numberDiff line change
@@ -73,15 +73,17 @@ new cloudfront.Distribution(this, 'myDist', {
7373

7474
When creating a standard S3 origin using `origins.S3BucketOrigin.withOriginAccessControl()`, an [Origin Access Control resource](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cloudfront-originaccesscontrol-originaccesscontrolconfig.html) is automatically created with the origin type set to `s3` and signing behavior set to `always`.
7575

76-
You can grant read, list, write or delete access to the OAC using the `originAccessLevels` property:
76+
You can grant read, read versioned, list, write or delete access to the OAC using the `originAccessLevels` property:
7777

7878
```ts
7979
const myBucket = new s3.Bucket(this, 'myBucket');
80-
const s3Origin = origins.S3BucketOrigin.withOriginAccessControl(myBucket, {
81-
originAccessLevels: [cloudfront.AccessLevel.READ, cloudfront.AccessLevel.WRITE, cloudfront.AccessLevel.DELETE],
80+
const s3Origin = origins.S3BucketOrigin.withOriginAccessControl(myBucket, { originAccessLevels: [cloudfront.AccessLevel.READ, cloudfront.AccessLevel.READ_VERSIONED, cloudfront.AccessLevel.WRITE, cloudfront.AccessLevel.DELETE],
8281
});
8382
```
8483

84+
The read versioned permission does contain the read permission, so it's required to set both `AccessLevel.READ` and
85+
`AccessLevel.READ_VERSIONED`.
86+
8587
For details of list permission, see [Setting up OAC with LIST permission](#setting-up-oac-with-list-permission).
8688

8789
You can also pass in a custom S3 origin access control:

packages/aws-cdk-lib/aws-cloudfront-origins/lib/s3-bucket-origin.ts

+39-33
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,11 @@ interface BucketPolicyAction {
1313

1414
const BUCKET_ACTIONS: Record<string, BucketPolicyAction[]> = {
1515
READ: [{ action: 's3:GetObject' }],
16-
LIST: [{ action: 's3:ListBucket', needsBucketArn: true }],
16+
READ_VERSIONED: [{ action: 's3:GetObjectVersion' }],
17+
LIST: [{
18+
action: 's3:ListBucket',
19+
needsBucketArn: true,
20+
}],
1721
WRITE: [{ action: 's3:PutObject' }],
1822
DELETE: [{ action: 's3:DeleteObject' }],
1923
};
@@ -26,25 +30,26 @@ const KEY_ACTIONS: Record<string, string[]> = {
2630
/**
2731
* Properties for configuring a origin using a standard S3 bucket
2832
*/
29-
export interface S3BucketOriginBaseProps extends cloudfront.OriginProps { }
33+
export interface S3BucketOriginBaseProps extends cloudfront.OriginProps {
34+
}
3035

3136
/**
3237
* Properties for configuring a S3 origin with OAC
3338
*/
3439
export interface S3BucketOriginWithOACProps extends S3BucketOriginBaseProps {
3540
/**
36-
* An optional Origin Access Control
37-
*
38-
* @default - an Origin Access Control will be created.
39-
*/
41+
* An optional Origin Access Control
42+
*
43+
* @default - an Origin Access Control will be created.
44+
*/
4045
readonly originAccessControl?: cloudfront.IOriginAccessControl;
4146

4247
/**
43-
* The level of permissions granted in the bucket policy and key policy (if applicable)
44-
* to the CloudFront distribution.
45-
*
46-
* @default [AccessLevel.READ]
47-
*/
48+
* The level of permissions granted in the bucket policy and key policy (if applicable)
49+
* to the CloudFront distribution.
50+
*
51+
* @default [AccessLevel.READ]
52+
*/
4853
readonly originAccessLevels?: AccessLevel[];
4954
}
5055

@@ -53,10 +58,10 @@ export interface S3BucketOriginWithOACProps extends S3BucketOriginBaseProps {
5358
*/
5459
export interface S3BucketOriginWithOAIProps extends S3BucketOriginBaseProps {
5560
/**
56-
* An optional Origin Access Identity
57-
*
58-
* @default - an Origin Access Identity will be created.
59-
*/
61+
* An optional Origin Access Identity
62+
*
63+
* @default - an Origin Access Identity will be created.
64+
*/
6065
readonly originAccessIdentity?: cloudfront.IOriginAccessIdentity;
6166
}
6267

@@ -65,24 +70,24 @@ export interface S3BucketOriginWithOAIProps extends S3BucketOriginBaseProps {
6570
*/
6671
export abstract class S3BucketOrigin extends cloudfront.OriginBase {
6772
/**
68-
* Create a S3 Origin with Origin Access Control (OAC) configured
69-
*/
73+
* Create a S3 Origin with Origin Access Control (OAC) configured
74+
*/
7075
public static withOriginAccessControl(bucket: IBucket, props?: S3BucketOriginWithOACProps): cloudfront.IOrigin {
7176
return new S3BucketOriginWithOAC(bucket, props);
7277
}
7378

7479
/**
75-
* Create a S3 Origin with Origin Access Identity (OAI) configured
76-
* OAI is a legacy feature and we **strongly** recommend you to use OAC via `withOriginAccessControl()`
77-
* unless it is not supported in your required region (e.g. China regions).
78-
*/
80+
* Create a S3 Origin with Origin Access Identity (OAI) configured
81+
* OAI is a legacy feature and we **strongly** recommend you to use OAC via `withOriginAccessControl()`
82+
* unless it is not supported in your required region (e.g. China regions).
83+
*/
7984
public static withOriginAccessIdentity(bucket: IBucket, props?: S3BucketOriginWithOAIProps): cloudfront.IOrigin {
8085
return new S3BucketOriginWithOAI(bucket, props);
8186
}
8287

8388
/**
84-
* Create a S3 Origin with default S3 bucket settings (no origin access control)
85-
*/
89+
* Create a S3 Origin with default S3 bucket settings (no origin access control)
90+
*/
8691
public static withBucketDefaults(bucket: IBucket, props?: cloudfront.OriginProps): cloudfront.IOrigin {
8792
return new class extends S3BucketOrigin {
8893
constructor() {
@@ -126,9 +131,9 @@ class S3BucketOriginWithOAC extends S3BucketOrigin {
126131
const accessLevels = new Set(this.originAccessLevels ?? [cloudfront.AccessLevel.READ]);
127132
if (accessLevels.has(AccessLevel.LIST)) {
128133
Annotations.of(scope).addWarningV2('@aws-cdk/aws-cloudfront-origins:listBucketSecurityRisk',
129-
'When the origin with AccessLevel.LIST is associated to the default behavior, '+
130-
'it is strongly recommended to ensure the distribution\'s defaultRootObject is specified,\n'+
131-
'See the "Setting up OAC with LIST permission" section of module\'s README for more info.');
134+
'When the origin with AccessLevel.LIST is associated to the default behavior, ' +
135+
'it is strongly recommended to ensure the distribution\'s defaultRootObject is specified,\n' +
136+
'See the "Setting up OAC with LIST permission" section of module\'s README for more info.');
132137
}
133138

134139
const bucketPolicyActions = this.getBucketPolicyActions(accessLevels);
@@ -138,7 +143,7 @@ class S3BucketOriginWithOAC extends S3BucketOrigin {
138143
if (!bucketPolicyResult.statementAdded) {
139144
Annotations.of(scope).addWarningV2('@aws-cdk/aws-cloudfront-origins:updateImportedBucketPolicyOac',
140145
'Cannot update bucket policy of an imported bucket. You will need to update the policy manually instead.\n' +
141-
'See the "Setting up OAC with imported S3 buckets" section of module\'s README for more info.');
146+
'See the "Setting up OAC with imported S3 buckets" section of module\'s README for more info.');
142147
}
143148

144149
if (this.bucket.encryptionKey) {
@@ -148,7 +153,7 @@ class S3BucketOriginWithOAC extends S3BucketOrigin {
148153
if (!keyPolicyResult.statementAdded) {
149154
Annotations.of(scope).addWarningV2('@aws-cdk/aws-cloudfront-origins:updateImportedKeyPolicyOac',
150155
'Cannot update key policy of an imported key. You will need to update the policy manually instead.\n' +
151-
'See the "Updating imported key policies" section of the module\'s README for more info.');
156+
'See the "Updating imported key policies" section of the module\'s README for more info.');
152157
}
153158
}
154159

@@ -210,9 +215,9 @@ class S3BucketOriginWithOAC extends S3BucketOrigin {
210215
);
211216
Annotations.of(key.node.scope!).addWarningV2('@aws-cdk/aws-cloudfront-origins:wildcardKeyPolicyForOac',
212217
'To avoid a circular dependency between the KMS key, Bucket, and Distribution during the initial deployment, ' +
213-
'a wildcard is used in the Key policy condition to match all Distribution IDs.\n' +
214-
'After deploying once, it is strongly recommended to further scope down the policy for best security practices by ' +
215-
'following the guidance in the "Using OAC for a SSE-KMS encrypted S3 origin" section in the module README.');
218+
'a wildcard is used in the Key policy condition to match all Distribution IDs.\n' +
219+
'After deploying once, it is strongly recommended to further scope down the policy for best security practices by ' +
220+
'following the guidance in the "Using OAC for a SSE-KMS encrypted S3 origin" section in the module README.');
216221
const result = key.addToResourcePolicy(oacKeyPolicyStatement);
217222
return result;
218223
}
@@ -242,7 +247,8 @@ class S3BucketOriginWithOAI extends S3BucketOrigin {
242247
this.originAccessIdentity = new cloudfront.OriginAccessIdentity(oaiScope, oaiId, {
243248
comment: `Identity for ${options.originId}`,
244249
});
245-
};
250+
}
251+
;
246252
// Used rather than `grantRead` because `grantRead` will grant overly-permissive policies.
247253
// Only GetObject is needed to retrieve objects for the distribution.
248254
// This also excludes KMS permissions; OAI only supports SSE-S3 for buckets.
@@ -255,7 +261,7 @@ class S3BucketOriginWithOAI extends S3BucketOrigin {
255261
if (!result.statementAdded) {
256262
Annotations.of(scope).addWarningV2('@aws-cdk/aws-cloudfront-origins:updateImportedBucketPolicyOai',
257263
'Cannot update bucket policy of an imported bucket. You will need to update the policy manually instead.\n' +
258-
'See the "Setting up OAI with imported S3 buckets (legacy)" section of module\'s README for more info.');
264+
'See the "Setting up OAI with imported S3 buckets (legacy)" section of module\'s README for more info.');
259265
}
260266
return this._bind(scope, options);
261267
}

packages/aws-cdk-lib/aws-cloudfront-origins/test/s3-bucket-origin.test.ts

+53-11
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import * as cloudfront from '../../aws-cloudfront/index';
33
import * as origins from '../../aws-cloudfront-origins';
44
import * as kms from '../../aws-kms';
55
import * as s3 from '../../aws-s3/index';
6-
import { App, Duration, Fn, Stack } from '../../core';
6+
import { App, Duration, Stack } from '../../core';
77

88
describe('S3BucketOrigin', () => {
99
describe('withOriginAccessControl', () => {
@@ -380,9 +380,9 @@ describe('S3BucketOrigin', () => {
380380
});
381381
Annotations.fromStack(stack).hasWarning('/Default',
382382
'To avoid a circular dependency between the KMS key, Bucket, and Distribution during the initial deployment, ' +
383-
'a wildcard is used in the Key policy condition to match all Distribution IDs.\n' +
384-
'After deploying once, it is strongly recommended to further scope down the policy for best security practices by ' +
385-
'following the guidance in the "Using OAC for a SSE-KMS encrypted S3 origin" section in the module README. [ack: @aws-cdk/aws-cloudfront-origins:wildcardKeyPolicyForOac]');
383+
'a wildcard is used in the Key policy condition to match all Distribution IDs.\n' +
384+
'After deploying once, it is strongly recommended to further scope down the policy for best security practices by ' +
385+
'following the guidance in the "Using OAC for a SSE-KMS encrypted S3 origin" section in the module README. [ack: @aws-cdk/aws-cloudfront-origins:wildcardKeyPolicyForOac]');
386386
});
387387

388388
it('should allow users to use escape hatch to scope down KMS key policy to specific distribution id', () => {
@@ -475,7 +475,7 @@ describe('S3BucketOrigin', () => {
475475
});
476476
Annotations.fromStack(stack).hasWarning('/Default/MyDistributionA/Origin1',
477477
'Cannot update key policy of an imported key. You will need to update the policy manually instead.\n' +
478-
'See the "Updating imported key policies" section of the module\'s README for more info. [ack: @aws-cdk/aws-cloudfront-origins:updateImportedKeyPolicyOac]');
478+
'See the "Updating imported key policies" section of the module\'s README for more info. [ack: @aws-cdk/aws-cloudfront-origins:updateImportedKeyPolicyOac]');
479479
});
480480
});
481481

@@ -716,7 +716,7 @@ describe('S3BucketOrigin', () => {
716716
it('should warn user bucket policy is not updated', () => {
717717
Annotations.fromStack(stack).hasWarning('/Default/MyDistributionA/Origin1',
718718
'Cannot update bucket policy of an imported bucket. You will need to update the policy manually instead.\n' +
719-
'See the "Setting up OAC with imported S3 buckets" section of module\'s README for more info. [ack: @aws-cdk/aws-cloudfront-origins:updateImportedBucketPolicyOac]');
719+
'See the "Setting up OAC with imported S3 buckets" section of module\'s README for more info. [ack: @aws-cdk/aws-cloudfront-origins:updateImportedBucketPolicyOac]');
720720
});
721721

722722
it('should match expected template resources', () => {
@@ -893,6 +893,48 @@ describe('S3BucketOrigin', () => {
893893
});
894894
});
895895

896+
describe('when specifying READ and READ_VERSIONED origin access levels', () => {
897+
it('should add the correct permissions to bucket policy', () => {
898+
const stack = new Stack();
899+
const bucket = new s3.Bucket(stack, 'MyBucket');
900+
const origin = origins.S3BucketOrigin.withOriginAccessControl(bucket, {
901+
originAccessLevels: [cloudfront.AccessLevel.READ, cloudfront.AccessLevel.READ_VERSIONED],
902+
});
903+
new cloudfront.Distribution(stack, 'MyDistribution', {
904+
defaultBehavior: { origin },
905+
});
906+
907+
Template.fromStack(stack).hasResourceProperties('AWS::S3::BucketPolicy', {
908+
PolicyDocument: {
909+
Statement: [
910+
{
911+
Action: ['s3:GetObject', 's3:GetObjectVersion'],
912+
Effect: 'Allow',
913+
Principal: { Service: 'cloudfront.amazonaws.com' },
914+
Condition: {
915+
StringEquals: {
916+
'AWS:SourceArn': {
917+
'Fn::Join': [
918+
'',
919+
[
920+
'arn:',
921+
{ Ref: 'AWS::Partition' },
922+
':cloudfront::',
923+
{ Ref: 'AWS::AccountId' },
924+
':distribution/',
925+
{ Ref: 'MyDistribution6271DFB5' },
926+
],
927+
],
928+
},
929+
},
930+
},
931+
Resource: { 'Fn::Join': ['', [{ 'Fn::GetAtt': ['MyBucketF68F3FF0', 'Arn'] }, '/*']] },
932+
},
933+
],
934+
},
935+
});
936+
});
937+
});
896938
it('should add the warning annotation', () => {
897939
const stack = new Stack();
898940
const bucket = new s3.Bucket(stack, 'MyBucket');
@@ -903,10 +945,10 @@ describe('S3BucketOrigin', () => {
903945
defaultBehavior: { origin },
904946
});
905947
Annotations.fromStack(stack).hasWarning('/Default/MyDistribution/Origin1',
906-
'When the origin with AccessLevel.LIST is associated to the default behavior, '+
907-
'it is strongly recommended to ensure the distribution\'s defaultRootObject is specified,\n'+
908-
'See the "Setting up OAC with LIST permission" section of module\'s README for more info.'+
909-
' [ack: @aws-cdk/aws-cloudfront-origins:listBucketSecurityRisk]');
948+
'When the origin with AccessLevel.LIST is associated to the default behavior, ' +
949+
'it is strongly recommended to ensure the distribution\'s defaultRootObject is specified,\n' +
950+
'See the "Setting up OAC with LIST permission" section of module\'s README for more info.' +
951+
' [ack: @aws-cdk/aws-cloudfront-origins:listBucketSecurityRisk]');
910952
});
911953
});
912954

@@ -1229,7 +1271,7 @@ describe('S3BucketOrigin', () => {
12291271
it('should warn user bucket policy is not updated', () => {
12301272
Annotations.fromStack(distributionStack).hasWarning('/distributionStack/MyDistributionA/Origin1',
12311273
'Cannot update bucket policy of an imported bucket. You will need to update the policy manually instead.\n' +
1232-
'See the "Setting up OAI with imported S3 buckets (legacy)" section of module\'s README for more info. [ack: @aws-cdk/aws-cloudfront-origins:updateImportedBucketPolicyOai]');
1274+
'See the "Setting up OAI with imported S3 buckets (legacy)" section of module\'s README for more info. [ack: @aws-cdk/aws-cloudfront-origins:updateImportedBucketPolicyOai]');
12331275
});
12341276

12351277
it('should create OAI in bucket stack and output it, then reference the output in the distribution stack', () => {

0 commit comments

Comments
 (0)