Skip to content

Commit

Permalink
Add CloudFront AccessLevel.READ_VERSIONED
Browse files Browse the repository at this point in the history
This allows creating an S3 bucket origin OriginAccessControl for access of versioned objects

Fixes #33034
  • Loading branch information
matthiasgubler committed Jan 21, 2025
1 parent a928748 commit 7aaae57
Show file tree
Hide file tree
Showing 5 changed files with 201 additions and 119 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import * as cloudfront from 'aws-cdk-lib/aws-cloudfront';
import * as s3 from 'aws-cdk-lib/aws-s3';
import * as cdk from 'aws-cdk-lib';
import * as origins from 'aws-cdk-lib/aws-cloudfront-origins';
import { ExpectedResult, IntegTest } from '@aws-cdk/integ-tests-alpha';

const app = new cdk.App();

const stack = new cdk.Stack(app, 'cloudfront-s3-bucket-origin-oac-read-versioned-access');

const bucket = new s3.Bucket(stack, 'Bucket', {
removalPolicy: cdk.RemovalPolicy.DESTROY,
});
origins.S3BucketOrigin.withOriginAccessControl(bucket, {
originAccessLevels: [cloudfront.AccessLevel.READ, cloudfront.AccessLevel.READ_VERSIONED],
});

const integ = new IntegTest(app, 's3-origin-oac-read-versioned-access', {
testCases: [stack],
});

integ.assertions.awsApiCall('S3', 'getBucketPolicy', {
Bucket: bucket.bucketName,
}).expect(ExpectedResult.objectLike({ Statement: [{ Action: ['s3:GetObject', 's3:GetObjectVersion'] }] }));
8 changes: 5 additions & 3 deletions packages/aws-cdk-lib/aws-cloudfront-origins/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,15 +73,17 @@ new cloudfront.Distribution(this, 'myDist', {

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`.

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

```ts
const myBucket = new s3.Bucket(this, 'myBucket');
const s3Origin = origins.S3BucketOrigin.withOriginAccessControl(myBucket, {
originAccessLevels: [cloudfront.AccessLevel.READ, cloudfront.AccessLevel.WRITE, cloudfront.AccessLevel.DELETE],
const s3Origin = origins.S3BucketOrigin.withOriginAccessControl(myBucket, { originAccessLevels: [cloudfront.AccessLevel.READ, cloudfront.AccessLevel.READ_VERSIONED, cloudfront.AccessLevel.WRITE, cloudfront.AccessLevel.DELETE],
});
```

The read versioned permission does contain the read permission, so it's required to set both `AccessLevel.READ` and
`AccessLevel.READ_VERSIONED`.

For details of list permission, see [Setting up OAC with LIST permission](#setting-up-oac-with-list-permission).

You can also pass in a custom S3 origin access control:
Expand Down
72 changes: 39 additions & 33 deletions packages/aws-cdk-lib/aws-cloudfront-origins/lib/s3-bucket-origin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@ interface BucketPolicyAction {

const BUCKET_ACTIONS: Record<string, BucketPolicyAction[]> = {
READ: [{ action: 's3:GetObject' }],
LIST: [{ action: 's3:ListBucket', needsBucketArn: true }],
READ_VERSIONED: [{ action: 's3:GetObjectVersion' }],
LIST: [{
action: 's3:ListBucket',
needsBucketArn: true,
}],
WRITE: [{ action: 's3:PutObject' }],
DELETE: [{ action: 's3:DeleteObject' }],
};
Expand All @@ -26,25 +30,26 @@ const KEY_ACTIONS: Record<string, string[]> = {
/**
* Properties for configuring a origin using a standard S3 bucket
*/
export interface S3BucketOriginBaseProps extends cloudfront.OriginProps { }
export interface S3BucketOriginBaseProps extends cloudfront.OriginProps {
}

/**
* Properties for configuring a S3 origin with OAC
*/
export interface S3BucketOriginWithOACProps extends S3BucketOriginBaseProps {
/**
* An optional Origin Access Control
*
* @default - an Origin Access Control will be created.
*/
* An optional Origin Access Control
*
* @default - an Origin Access Control will be created.
*/
readonly originAccessControl?: cloudfront.IOriginAccessControl;

/**
* The level of permissions granted in the bucket policy and key policy (if applicable)
* to the CloudFront distribution.
*
* @default [AccessLevel.READ]
*/
* The level of permissions granted in the bucket policy and key policy (if applicable)
* to the CloudFront distribution.
*
* @default [AccessLevel.READ]
*/
readonly originAccessLevels?: AccessLevel[];
}

Expand All @@ -53,10 +58,10 @@ export interface S3BucketOriginWithOACProps extends S3BucketOriginBaseProps {
*/
export interface S3BucketOriginWithOAIProps extends S3BucketOriginBaseProps {
/**
* An optional Origin Access Identity
*
* @default - an Origin Access Identity will be created.
*/
* An optional Origin Access Identity
*
* @default - an Origin Access Identity will be created.
*/
readonly originAccessIdentity?: cloudfront.IOriginAccessIdentity;
}

Expand All @@ -65,24 +70,24 @@ export interface S3BucketOriginWithOAIProps extends S3BucketOriginBaseProps {
*/
export abstract class S3BucketOrigin extends cloudfront.OriginBase {
/**
* Create a S3 Origin with Origin Access Control (OAC) configured
*/
* Create a S3 Origin with Origin Access Control (OAC) configured
*/
public static withOriginAccessControl(bucket: IBucket, props?: S3BucketOriginWithOACProps): cloudfront.IOrigin {
return new S3BucketOriginWithOAC(bucket, props);
}

/**
* Create a S3 Origin with Origin Access Identity (OAI) configured
* OAI is a legacy feature and we **strongly** recommend you to use OAC via `withOriginAccessControl()`
* unless it is not supported in your required region (e.g. China regions).
*/
* Create a S3 Origin with Origin Access Identity (OAI) configured
* OAI is a legacy feature and we **strongly** recommend you to use OAC via `withOriginAccessControl()`
* unless it is not supported in your required region (e.g. China regions).
*/
public static withOriginAccessIdentity(bucket: IBucket, props?: S3BucketOriginWithOAIProps): cloudfront.IOrigin {
return new S3BucketOriginWithOAI(bucket, props);
}

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

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

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

Expand Down Expand Up @@ -210,9 +215,9 @@ class S3BucketOriginWithOAC extends S3BucketOrigin {
);
Annotations.of(key.node.scope!).addWarningV2('@aws-cdk/aws-cloudfront-origins:wildcardKeyPolicyForOac',
'To avoid a circular dependency between the KMS key, Bucket, and Distribution during the initial deployment, ' +
'a wildcard is used in the Key policy condition to match all Distribution IDs.\n' +
'After deploying once, it is strongly recommended to further scope down the policy for best security practices by ' +
'following the guidance in the "Using OAC for a SSE-KMS encrypted S3 origin" section in the module README.');
'a wildcard is used in the Key policy condition to match all Distribution IDs.\n' +
'After deploying once, it is strongly recommended to further scope down the policy for best security practices by ' +
'following the guidance in the "Using OAC for a SSE-KMS encrypted S3 origin" section in the module README.');
const result = key.addToResourcePolicy(oacKeyPolicyStatement);
return result;
}
Expand Down Expand Up @@ -242,7 +247,8 @@ class S3BucketOriginWithOAI extends S3BucketOrigin {
this.originAccessIdentity = new cloudfront.OriginAccessIdentity(oaiScope, oaiId, {
comment: `Identity for ${options.originId}`,
});
};
}
;
// Used rather than `grantRead` because `grantRead` will grant overly-permissive policies.
// Only GetObject is needed to retrieve objects for the distribution.
// This also excludes KMS permissions; OAI only supports SSE-S3 for buckets.
Expand All @@ -255,7 +261,7 @@ class S3BucketOriginWithOAI extends S3BucketOrigin {
if (!result.statementAdded) {
Annotations.of(scope).addWarningV2('@aws-cdk/aws-cloudfront-origins:updateImportedBucketPolicyOai',
'Cannot update bucket policy of an imported bucket. You will need to update the policy manually instead.\n' +
'See the "Setting up OAI with imported S3 buckets (legacy)" section of module\'s README for more info.');
'See the "Setting up OAI with imported S3 buckets (legacy)" section of module\'s README for more info.');
}
return this._bind(scope, options);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import * as cloudfront from '../../aws-cloudfront/index';
import * as origins from '../../aws-cloudfront-origins';
import * as kms from '../../aws-kms';
import * as s3 from '../../aws-s3/index';
import { App, Duration, Fn, Stack } from '../../core';
import { App, Duration, Stack } from '../../core';

describe('S3BucketOrigin', () => {
describe('withOriginAccessControl', () => {
Expand Down Expand Up @@ -380,9 +380,9 @@ describe('S3BucketOrigin', () => {
});
Annotations.fromStack(stack).hasWarning('/Default',
'To avoid a circular dependency between the KMS key, Bucket, and Distribution during the initial deployment, ' +
'a wildcard is used in the Key policy condition to match all Distribution IDs.\n' +
'After deploying once, it is strongly recommended to further scope down the policy for best security practices by ' +
'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]');
'a wildcard is used in the Key policy condition to match all Distribution IDs.\n' +
'After deploying once, it is strongly recommended to further scope down the policy for best security practices by ' +
'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]');
});

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

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

it('should match expected template resources', () => {
Expand Down Expand Up @@ -893,6 +893,48 @@ describe('S3BucketOrigin', () => {
});
});

describe('when specifying READ and READ_VERSIONED origin access levels', () => {
it('should add the correct permissions to bucket policy', () => {
const stack = new Stack();
const bucket = new s3.Bucket(stack, 'MyBucket');
const origin = origins.S3BucketOrigin.withOriginAccessControl(bucket, {
originAccessLevels: [cloudfront.AccessLevel.READ, cloudfront.AccessLevel.READ_VERSIONED],
});
new cloudfront.Distribution(stack, 'MyDistribution', {
defaultBehavior: { origin },
});

Template.fromStack(stack).hasResourceProperties('AWS::S3::BucketPolicy', {
PolicyDocument: {
Statement: [
{
Action: ['s3:GetObject', 's3:GetObjectVersion'],
Effect: 'Allow',
Principal: { Service: 'cloudfront.amazonaws.com' },
Condition: {
StringEquals: {
'AWS:SourceArn': {
'Fn::Join': [
'',
[
'arn:',
{ Ref: 'AWS::Partition' },
':cloudfront::',
{ Ref: 'AWS::AccountId' },
':distribution/',
{ Ref: 'MyDistribution6271DFB5' },
],
],
},
},
},
Resource: { 'Fn::Join': ['', [{ 'Fn::GetAtt': ['MyBucketF68F3FF0', 'Arn'] }, '/*']] },
},
],
},
});
});
});
it('should add the warning annotation', () => {
const stack = new Stack();
const bucket = new s3.Bucket(stack, 'MyBucket');
Expand All @@ -903,10 +945,10 @@ describe('S3BucketOrigin', () => {
defaultBehavior: { origin },
});
Annotations.fromStack(stack).hasWarning('/Default/MyDistribution/Origin1',
'When the origin with AccessLevel.LIST is associated to the default behavior, '+
'it is strongly recommended to ensure the distribution\'s defaultRootObject is specified,\n'+
'See the "Setting up OAC with LIST permission" section of module\'s README for more info.'+
' [ack: @aws-cdk/aws-cloudfront-origins:listBucketSecurityRisk]');
'When the origin with AccessLevel.LIST is associated to the default behavior, ' +
'it is strongly recommended to ensure the distribution\'s defaultRootObject is specified,\n' +
'See the "Setting up OAC with LIST permission" section of module\'s README for more info.' +
' [ack: @aws-cdk/aws-cloudfront-origins:listBucketSecurityRisk]');
});
});

Expand Down Expand Up @@ -1229,7 +1271,7 @@ describe('S3BucketOrigin', () => {
it('should warn user bucket policy is not updated', () => {
Annotations.fromStack(distributionStack).hasWarning('/distributionStack/MyDistributionA/Origin1',
'Cannot update bucket policy of an imported bucket. You will need to update the policy manually instead.\n' +
'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]');
'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]');
});

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

0 comments on commit 7aaae57

Please sign in to comment.