From 98da8324f1b637b5e6414f8cf480792ad24b27b7 Mon Sep 17 00:00:00 2001 From: gracelu0 Date: Fri, 17 May 2024 01:45:52 -0700 Subject: [PATCH 01/14] wip oac --- .../aws-cloudfront-origins/lib/s3-origin.ts | 130 +++++++++-- .../aws-cloudfront/lib/distribution.ts | 7 +- .../aws-cdk-lib/aws-cloudfront/lib/index.ts | 1 + .../lib/origin-access-control.ts | 207 ++++++++++++++++++ .../aws-cdk-lib/aws-cloudfront/lib/origin.ts | 15 ++ .../aws-cloudfront/lib/web-distribution.ts | 7 + packages/aws-cdk-lib/cx-api/FEATURE_FLAGS.md | 33 ++- packages/aws-cdk-lib/cx-api/README.md | 19 +- packages/aws-cdk-lib/cx-api/lib/features.ts | 15 +- 9 files changed, 405 insertions(+), 29 deletions(-) create mode 100644 packages/aws-cdk-lib/aws-cloudfront/lib/origin-access-control.ts diff --git a/packages/aws-cdk-lib/aws-cloudfront-origins/lib/s3-origin.ts b/packages/aws-cdk-lib/aws-cloudfront-origins/lib/s3-origin.ts index e57a91b110c27..edf4b37c15af5 100644 --- a/packages/aws-cdk-lib/aws-cloudfront-origins/lib/s3-origin.ts +++ b/packages/aws-cdk-lib/aws-cloudfront-origins/lib/s3-origin.ts @@ -3,7 +3,8 @@ import { HttpOrigin } from './http-origin'; import * as cloudfront from '../../aws-cloudfront'; import * as iam from '../../aws-iam'; import * as s3 from '../../aws-s3'; -import * as cdk from '../../core'; +import { Stack, Names, FeatureFlags, Aws, Lazy } from '../../core'; +import * as cxapi from '../../cx-api'; /** * Properties to use to customize an S3 Origin. @@ -15,6 +16,12 @@ export interface S3OriginProps extends cloudfront.OriginProps { * @default - An Origin Access Identity will be created. */ readonly originAccessIdentity?: cloudfront.IOriginAccessIdentity; + + /** + * An optional Origin Access Control + * @default - An Origin Access Control will be created. + */ + readonly originAccessControl?: cloudfront.IOriginAccessControl; } /** @@ -32,8 +39,7 @@ export class S3Origin implements cloudfront.IOrigin { new HttpOrigin(bucket.bucketWebsiteDomainName, { protocolPolicy: cloudfront.OriginProtocolPolicy.HTTP_ONLY, // S3 only supports HTTP for website buckets ...props, - }) : - new S3BucketOrigin(bucket, props); + }) : new S3BucketOrigin(bucket, props); } public bind(scope: Construct, options: cloudfront.OriginBindOptions): cloudfront.OriginBindConfig { @@ -41,49 +47,129 @@ export class S3Origin implements cloudfront.IOrigin { } } +/** + * An Origin specific to a S3 bucket (not configured for website hosting). + * + * Contains additional logic around bucket permissions and origin access controls. + */ +class S3BucketOacOrigin extends cloudfront.OriginBase { + private originAccessControl!: cloudfront.IOriginAccessControl; + + constructor(private readonly bucket: s3.IBucket, { originAccessControl, ...props }: S3OriginProps) { + super(bucket.bucketRegionalDomainName, props); + if (originAccessControl) { + this.originAccessControl = originAccessControl; + } + } + + public bind(scope: Construct, options: cloudfront.OriginBindOptions): cloudfront.OriginBindConfig { + if (!this.originAccessControl) { + this.originAccessControl = new cloudfront.OriginAccessControl(scope, options.originId); + } + return super.bind(scope, options); + } + + /** + * If you're using origin access control (OAC) instead of origin access identity, specify an empty `OriginAccessIdentity` element. + * @see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cloudfront-distribution-s3originconfig.html#cfn-cloudfront-distribution-s3originconfig-originaccessidentity + */ + protected renderS3OriginConfig(): cloudfront.CfnDistribution.S3OriginConfigProperty | undefined { + return { originAccessIdentity: '' }; + } +} + /** * An Origin specific to a S3 bucket (not configured for website hosting). * * Contains additional logic around bucket permissions and origin access identities. */ class S3BucketOrigin extends cloudfront.OriginBase { - private originAccessIdentity!: cloudfront.IOriginAccessIdentity; + private originAccessIdentity?: cloudfront.IOriginAccessIdentity; + private originAccessControl?: cloudfront.IOriginAccessControl; - constructor(private readonly bucket: s3.IBucket, { originAccessIdentity, ...props }: S3OriginProps) { + constructor(private readonly bucket: s3.IBucket, props: S3OriginProps) { super(bucket.bucketRegionalDomainName, props); - if (originAccessIdentity) { - this.originAccessIdentity = originAccessIdentity; + // if (originAccessIdentity) { + // this.originAccessIdentity = originAccessIdentity; + // } + if (props.originAccessControl && props.originAccessIdentity) { + throw new Error('Only one of originAccessControl or originAccessIdentity can be specified for an origin.'); } + this.originAccessControl = props.originAccessControl; + this.originAccessIdentity = props.originAccessIdentity; } public bind(scope: Construct, options: cloudfront.OriginBindOptions): cloudfront.OriginBindConfig { - if (!this.originAccessIdentity) { + if (FeatureFlags.of(scope).isEnabled(cxapi.CLOUDFRONT_USE_ORIGIN_ACCESS_CONTROL)) { + if (!this.originAccessControl) { + // Create a new origin access control if not specified + this.originAccessControl = new cloudfront.OriginAccessControl(scope, 'S3OriginAccessControl'); + } + const distribution = scope.node.scope as cloudfront.Distribution; + const distributionId = Lazy.string({ produce: () => distribution.distributionId }); + const oacReadOnlyBucketPolicyStatement = new iam.PolicyStatement( + { + sid: 'AllowS3OACAccess', + effect: iam.Effect.ALLOW, + principals: [new iam.ServicePrincipal('cloudfront.amazonaws.com')], + actions: ['s3:GetObject'], + resources: [this.bucket.arnForObjects('*')], + conditions: { + StringEquals: { + // eslint-disable-next-line @aws-cdk/no-literal-partition + 'AWS:SourceArn': `arn:aws:cloudfront::${Aws.ACCOUNT_ID}:distribution/${distributionId}`, + }, + }, + }, + ); + const result = this.bucket.addToResourcePolicy(oacReadOnlyBucketPolicyStatement); + + if (!result.statementAdded) { + throw new Error('Policy statement was not added to bucket policy'); + } + + const originBindConfig = super.bind(scope, options); + + // Update configuration to set OriginControlAccessId property + return { + ...originBindConfig, + originProperty: { + ...originBindConfig.originProperty!, + originAccessControlId: this.originAccessControl.originAccessControlId, + }, + }; + } else if (!this.originAccessIdentity && !(FeatureFlags.of(scope).isEnabled(cxapi.CLOUDFRONT_USE_ORIGIN_ACCESS_CONTROL))) { // Using a bucket from another stack creates a cyclic reference with // the bucket taking a dependency on the generated S3CanonicalUserId for the grant principal, // and the distribution having a dependency on the bucket's domain name. // Fix this by parenting the OAI in the bucket's stack when cross-stack usage is detected. - const bucketStack = cdk.Stack.of(this.bucket); - const bucketInDifferentStack = bucketStack !== cdk.Stack.of(scope); + const bucketStack = Stack.of(this.bucket); + const bucketInDifferentStack = bucketStack !== Stack.of(scope); const oaiScope = bucketInDifferentStack ? bucketStack : scope; - const oaiId = bucketInDifferentStack ? `${cdk.Names.uniqueId(scope)}S3Origin` : 'S3Origin'; + const oaiId = bucketInDifferentStack ? `${Names.uniqueId(scope)}S3Origin` : 'S3Origin'; 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; currently, OAI only supports SSE-S3 for buckets. - // Source: https://aws.amazon.com/blogs/networking-and-content-delivery/serving-sse-kms-encrypted-content-from-s3-using-cloudfront/ - this.bucket.addToResourcePolicy(new iam.PolicyStatement({ - resources: [this.bucket.arnForObjects('*')], - actions: ['s3:GetObject'], - principals: [this.originAccessIdentity.grantPrincipal], - })); + + // 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; currently, OAI only supports SSE-S3 for buckets. + // Source: https://aws.amazon.com/blogs/networking-and-content-delivery/serving-sse-kms-encrypted-content-from-s3-using-cloudfront/ + this.bucket.addToResourcePolicy(new iam.PolicyStatement({ + resources: [this.bucket.arnForObjects('*')], + actions: ['s3:GetObject'], + principals: [this.originAccessIdentity.grantPrincipal], + })); + } + return super.bind(scope, options); } protected renderS3OriginConfig(): cloudfront.CfnDistribution.S3OriginConfigProperty | undefined { - return { originAccessIdentity: `origin-access-identity/cloudfront/${this.originAccessIdentity.originAccessIdentityId}` }; + if (this.originAccessIdentity) { + return { originAccessIdentity: `origin-access-identity/cloudfront/${this.originAccessIdentity.originAccessIdentityId}` }; + } + return { originAccessIdentity: '' }; } } diff --git a/packages/aws-cdk-lib/aws-cloudfront/lib/distribution.ts b/packages/aws-cdk-lib/aws-cloudfront/lib/distribution.ts index 336affec8b862..cbda930c0fc83 100644 --- a/packages/aws-cdk-lib/aws-cloudfront/lib/distribution.ts +++ b/packages/aws-cdk-lib/aws-cloudfront/lib/distribution.ts @@ -608,21 +608,22 @@ export class Distribution extends Resource implements IDistribution { const originIndex = this.boundOrigins.length + 1; const scope = new Construct(this, `Origin${originIndex}`); const generatedId = Names.uniqueId(scope).slice(-ORIGIN_ID_MAX_LENGTH); - const originBindConfig = origin.bind(scope, { originId: generatedId }); + const distributionId = this.distributionId; + const originBindConfig = origin.bind(scope, { originId: generatedId, distributionId }); const originId = originBindConfig.originProperty?.id ?? generatedId; const duplicateId = this.boundOrigins.find(boundOrigin => boundOrigin.originProperty?.id === originBindConfig.originProperty?.id); if (duplicateId) { throw new Error(`Origin with id ${duplicateId.originProperty?.id} already exists. OriginIds must be unique within a distribution`); } if (!originBindConfig.failoverConfig) { - this.boundOrigins.push({ origin, originId, ...originBindConfig }); + this.boundOrigins.push({ origin, originId, distributionId, ...originBindConfig }); } else { if (isFailoverOrigin) { throw new Error('An Origin cannot use an Origin with its own failover configuration as its fallback origin!'); } const groupIndex = this.originGroups.length + 1; const originGroupId = Names.uniqueId(new Construct(this, `OriginGroup${groupIndex}`)).slice(-ORIGIN_ID_MAX_LENGTH); - this.boundOrigins.push({ origin, originId, originGroupId, ...originBindConfig }); + this.boundOrigins.push({ origin, originId, distributionId, originGroupId, ...originBindConfig }); const failoverOriginId = this.addOrigin(originBindConfig.failoverConfig.failoverOrigin, true); this.addOriginGroup(originGroupId, originBindConfig.failoverConfig.statusCodes, originId, failoverOriginId); diff --git a/packages/aws-cdk-lib/aws-cloudfront/lib/index.ts b/packages/aws-cdk-lib/aws-cloudfront/lib/index.ts index 5a7e2863c8a4e..41aef443a6660 100644 --- a/packages/aws-cdk-lib/aws-cloudfront/lib/index.ts +++ b/packages/aws-cdk-lib/aws-cloudfront/lib/index.ts @@ -12,6 +12,7 @@ export * from './public-key'; export * from './realtime-log-config'; export * from './response-headers-policy'; export * from './web-distribution'; +export * from './origin-access-control'; export * as experimental from './experimental'; diff --git a/packages/aws-cdk-lib/aws-cloudfront/lib/origin-access-control.ts b/packages/aws-cdk-lib/aws-cloudfront/lib/origin-access-control.ts new file mode 100644 index 0000000000000..b59b3d738640b --- /dev/null +++ b/packages/aws-cdk-lib/aws-cloudfront/lib/origin-access-control.ts @@ -0,0 +1,207 @@ +import { Construct } from 'constructs'; +import { CfnOriginAccessControl } from './cloudfront.generated'; +import { IResource, Resource, Stack, Names } from '../../core'; +/** + * Interface for CloudFront origin access controls + */ +// extends iam.IGrantable?? +export interface IOriginAccessControl extends IResource { + /** + * The unique identifier of the origin access control. + * @attribute + */ + readonly originAccessControlId: string; + + /** + * The name of the origin access control. + * @attribute + */ + readonly originAccessControlName: string; +} + +abstract class OriginAccessControlBase extends Resource implements IOriginAccessControl { + public abstract readonly originAccessControlId: string; + public abstract readonly originAccessControlName: string; +} + +/** + * Properties for creating a OriginAccessControl resource. + */ +export interface OriginAccessControlProps { + /** + * A description of the origin access control. + * @default - no description + */ + readonly description?: string; + /** + * A name to identify the origin access control. You can specify up to 64 characters. + * @default - a generated name + */ + readonly originAccessControlName?: string; + /** + * The type of origin that this origin access control is for. + * @default s3 + */ + readonly originAccessControlOriginType?: OriginAccessControlOriginType; + /** + * Specifies which requests CloudFront signs. + * @default always + */ + readonly signingBehavior?: SigningBehavior; + /** + * The signing protocol of the origin access control. + * @default sigv4 + */ + readonly signingProtocol?: SigningProtocol; +} + +/** + * Origin types supported by origin access control. + */ +export enum OriginAccessControlOriginType { + /** + * Uses an Amazon S3 bucket origin. + */ + S3 = 's3', + /** + * Uses an AWS Elemental MediaStore origin. + */ + MEDIASTORE = 'mediastore', + /** + * Uses a Lambda function URL origin. + */ + LAMBDA = 'lambda', + /** + * Uses an AWS Elemental MediaPackage v2 origin. + */ + MEDIAPACKAGEV2 = 'mediapackagev2', +} + +/** + * Options for which requests CloudFront signs. + * Specify `always` for the most common use case. + */ +export enum SigningBehavior { + /** + * Sign all origin requests, overwriting the Authorization header + * from the viewer request if one exists. + */ + ALWAYS = 'always', + /** + * Do not sign any origin requests. + * This value turns off origin access control for all origins in all + * distributions that use this origin access control. + */ + NEVER = 'never', + /** + * Sign origin requests only if the viewer request + * doesn't contain the Authorization header. + */ + NO_OVERRIDE = 'no-override', +} + +/** + * The signing protocol of the origin access control. + */ +export enum SigningProtocol { + /** + * The AWS Signature Version 4 signing protocol. + */ + SIGV4 = 'sigv4', +} + +/** + * An Origin Access Control. + * @resource AWS::CloudFront::OriginAccessControl + * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-cloudfront-originaccesscontrol.html + */ +export class OriginAccessControl extends OriginAccessControlBase { + /** + * Imports an origin access control from its id. + */ + public static fromOriginAccessControlId(scope: Construct, id: string, originAccessControlId: string): IOriginAccessControl { + class Import extends OriginAccessControlBase { + public readonly originAccessControlId = originAccessControlId; + public readonly originAccessControlName = originAccessControlId; + constructor(s: Construct, i: string) { + super(s, i); + + this.originAccessControlId = originAccessControlId; + this.originAccessControlName = originAccessControlId; + } + } + return new Import(scope, id); + } + /** + * docstring + */ + public static s3OriginAccessControl(scope: Construct, id: string, props?: OriginAccessControlProps): IOriginAccessControl { + return new OriginAccessControl(scope, id, { + description: props?.description, + originAccessControlName: props?.originAccessControlName, + originAccessControlOriginType: OriginAccessControlOriginType.S3, + }); + } + + /** + * docstring + */ + public static mediastoreOriginAccessControl(scope: Construct, id: string, props?: OriginAccessControlProps): IOriginAccessControl { + return new OriginAccessControl(scope, id, { + description: props?.description, + originAccessControlName: props?.originAccessControlName, + originAccessControlOriginType: OriginAccessControlOriginType.MEDIASTORE, + }); + } + + /** + * docstring + */ + public static lambdaOriginAccessControl(scope: Construct, id: string, props?: OriginAccessControlProps): IOriginAccessControl { + return new OriginAccessControl(scope, id, { + description: props?.description, + originAccessControlName: props?.originAccessControlName, + originAccessControlOriginType: OriginAccessControlOriginType.LAMBDA, + }); + } + + /** + * docstring + */ + public static mediapackageOriginAccessControl(scope: Construct, id: string, props?: OriginAccessControlProps): IOriginAccessControl { + return new OriginAccessControl(scope, id, { + description: props?.description, + originAccessControlName: props?.originAccessControlName, + originAccessControlOriginType: OriginAccessControlOriginType.MEDIAPACKAGEV2, + }); + } + + public readonly originAccessControlId: string; + public readonly originAccessControlName: string; + + constructor(scope: Construct, id: string, props: OriginAccessControlProps = {}) { + super(scope, id); + + const resource = new CfnOriginAccessControl(this, 'Resource', { + originAccessControlConfig: { + description: props.description, + name: props.originAccessControlName ?? this.generateName(), + signingBehavior: props.signingBehavior ?? SigningBehavior.ALWAYS, + signingProtocol: props.signingProtocol ?? SigningProtocol.SIGV4, + originAccessControlOriginType: props.originAccessControlOriginType ?? OriginAccessControlOriginType.S3, + }, + }); + + this.originAccessControlId = resource.attrId; + this.originAccessControlName = resource.ref; + } + + private generateName(): string { + const name = Stack.of(this).region + Names.uniqueId(this); + if (name.length > 64) { + return name.substring(0, 32) + name.substring(name.length - 32); + } + return name; + } + +} \ No newline at end of file diff --git a/packages/aws-cdk-lib/aws-cloudfront/lib/origin.ts b/packages/aws-cdk-lib/aws-cloudfront/lib/origin.ts index 2044cbc5fe489..049e9bb80a56a 100644 --- a/packages/aws-cdk-lib/aws-cloudfront/lib/origin.ts +++ b/packages/aws-cdk-lib/aws-cloudfront/lib/origin.ts @@ -95,6 +95,13 @@ export interface OriginOptions { * @default - an originid will be generated for you */ readonly originId?: string; + + /** + * The unique identifier of an origin access control for this origin. + * + * @default - no origin access control + */ + readonly originAccessControlId?: string; } /** @@ -119,6 +126,11 @@ export interface OriginBindOptions { * as assigned by the Distribution this Origin has been used added to. */ readonly originId: string; + + /** + * The identifier of the Distribution this Origin is used for. + */ + readonly distributionId: string; } /** @@ -134,6 +146,7 @@ export abstract class OriginBase implements IOrigin { private readonly originShieldRegion?: string; private readonly originShieldEnabled: boolean; private readonly originId?: string; + private readonly originAccessControlId?: string; protected constructor(domainName: string, props: OriginProps = {}) { validateIntInRangeOrUndefined('connectionTimeout', 1, 10, props.connectionTimeout?.toSeconds()); @@ -148,6 +161,7 @@ export abstract class OriginBase implements IOrigin { this.originShieldRegion = props.originShieldRegion; this.originId = props.originId; this.originShieldEnabled = props.originShieldEnabled ?? true; + this.originAccessControlId = props.originAccessControlId; } /** @@ -172,6 +186,7 @@ export abstract class OriginBase implements IOrigin { s3OriginConfig, customOriginConfig, originShield: this.renderOriginShield(this.originShieldEnabled, this.originShieldRegion), + originAccessControlId: this.originAccessControlId, }, }; } diff --git a/packages/aws-cdk-lib/aws-cloudfront/lib/web-distribution.ts b/packages/aws-cdk-lib/aws-cloudfront/lib/web-distribution.ts index c0fd28b157416..dcb120fe3bc26 100644 --- a/packages/aws-cdk-lib/aws-cloudfront/lib/web-distribution.ts +++ b/packages/aws-cdk-lib/aws-cloudfront/lib/web-distribution.ts @@ -4,6 +4,7 @@ import { HttpVersion, IDistribution, LambdaEdgeEventType, OriginProtocolPolicy, import { FunctionAssociation } from './function'; import { GeoRestriction } from './geo-restriction'; import { IKeyGroup } from './key-group'; +import { IOriginAccessControl } from './origin-access-control'; import { IOriginAccessIdentity } from './origin-access-identity'; import { formatDistributionArn } from './private/utils'; import * as certificatemanager from '../../aws-certificatemanager'; @@ -213,6 +214,12 @@ export interface SourceConfiguration { * @default - origin shield not enabled */ readonly originShieldRegion?: string; + + /** + * Origin Access Control + * @default - No origin access control + */ + readonly originAccessControl?: IOriginAccessControl; } /** diff --git a/packages/aws-cdk-lib/cx-api/FEATURE_FLAGS.md b/packages/aws-cdk-lib/cx-api/FEATURE_FLAGS.md index e67806c3ceca1..59e79334b7042 100644 --- a/packages/aws-cdk-lib/cx-api/FEATURE_FLAGS.md +++ b/packages/aws-cdk-lib/cx-api/FEATURE_FLAGS.md @@ -70,8 +70,15 @@ Flags come in three types: | [@aws-cdk/aws-eks:nodegroupNameAttribute](#aws-cdkaws-eksnodegroupnameattribute) | When enabled, nodegroupName attribute of the provisioned EKS NodeGroup will not have the cluster name prefix. | 2.139.0 | (fix) | | [@aws-cdk/aws-ec2:ebsDefaultGp3Volume](#aws-cdkaws-ec2ebsdefaultgp3volume) | When enabled, the default volume type of the EBS volume will be GP3 | 2.140.0 | (default) | | [@aws-cdk/pipelines:reduceAssetRoleTrustScope](#aws-cdkpipelinesreduceassetroletrustscope) | Remove the root account principal from PipelineAssetsFileRole trust policy | 2.141.0 | (default) | +<<<<<<< HEAD | [@aws-cdk/aws-ecs:removeDefaultDeploymentAlarm](#aws-cdkaws-ecsremovedefaultdeploymentalarm) | When enabled, remove default deployment alarm settings | 2.143.0 | (default) | +<<<<<<< HEAD | [@aws-cdk/custom-resources:logApiResponseDataPropertyTrueDefault](#aws-cdkcustom-resourceslogapiresponsedatapropertytruedefault) | When enabled, the custom resource used for `AwsCustomResource` will configure the `logApiResponseData` property as true by default | 2.145.0 | (fix) | +======= +======= +| [@aws-cdk/aws-cloudfront:useOriginAccessControl](#aws-cdkaws-cloudfrontuseoriginaccesscontrol) | When enabled, use Origin Access Control rather than Origin Access Identity | V2NEXT | (fix) | +>>>>>>> 386904900a (wip oac) +>>>>>>> c47258bd5d (wip oac) @@ -132,8 +139,16 @@ The following json shows the current recommended set of flags, as `cdk init` wou "@aws-cdk/aws-kms:reduceCrossAccountRegionPolicyScope": true, "@aws-cdk/aws-eks:nodegroupNameAttribute": true, "@aws-cdk/aws-ec2:ebsDefaultGp3Volume": true, +<<<<<<< HEAD "@aws-cdk/aws-ecs:removeDefaultDeploymentAlarm": true, "@aws-cdk/custom-resources:logApiResponseDataPropertyTrueDefault": false +======= +<<<<<<< HEAD + "@aws-cdk/aws-ecs:removeDefaultDeploymentAlarm": true +======= + "@aws-cdk/aws-cloudfront:useOriginAccessControl": true, +>>>>>>> 386904900a (wip oac) +>>>>>>> c47258bd5d (wip oac) } } ``` @@ -1325,7 +1340,7 @@ When this feature flag is disabled, it will keep the root account principal in t *When enabled, remove default deployment alarm settings* (default) -When this featuer flag is enabled, remove the default deployment alarm settings when creating a AWS ECS service. +When this feature flag is enabled, remove the default deployment alarm settings when creating a AWS ECS service. | Since | Default | Recommended | @@ -1349,11 +1364,25 @@ Unlike most feature flags, we don't recommend setting this feature flag to true. the event object, then setting this feature flag will keep this behavior. Otherwise, setting this feature flag to false will trigger an 'Update' event by removing the 'logApiResponseData' property from the event object. - | Since | Default | Recommended | | ----- | ----- | ----- | | (not in v1) | | | | 2.145.0 | `false` | `false` | +### @aws-cdk/aws-cloudfront:useOriginAccessControl + +*Use Origin Access Control instead of Origin Access Identity* (fix) + +When this feature flag is enabled, an origin access control will be created automatically when a new S3 origin is created. +When this feature flag is disabled, an origin access identity will be created automatically when a new S3 origin is created. + + +| Since | Default | Recommended | +| ----- | ----- | ----- | +| (not in v1) | | | +| V2NEXT | `false` | `true` | + +**Compatibility with old behavior:** Disable the feature flag to continue using Origin Access Identity + diff --git a/packages/aws-cdk-lib/cx-api/README.md b/packages/aws-cdk-lib/cx-api/README.md index 2aca6f69abaf9..e0528747b760d 100644 --- a/packages/aws-cdk-lib/cx-api/README.md +++ b/packages/aws-cdk-lib/cx-api/README.md @@ -331,7 +331,7 @@ _cdk.json_ When enabled, the default volume type of the EBS volume will be GP3. -When this featuer flag is enabled, the default volume type of the EBS volume will be `EbsDeviceVolumeType.GENERAL_PURPOSE_SSD_GP3` +When this feature flag is enabled, the default volume type of the EBS volume will be `EbsDeviceVolumeType.GENERAL_PURPOSE_SSD_GP3` _cdk.json_ @@ -376,3 +376,20 @@ _cdk.json_ } } ``` + +* `@aws-cdk/aws-cloudfront:useOriginAccessControl` + +Use Origin Access Control instead of Origin Access Identity + +When this feature flag is enabled, an origin access control will be created automatically when a new S3 origin is created. + + +_cdk.json_ + +```json +{ + "context": { + "@aws-cdk/aws-cloudfront:useOriginAccessControl": true + } +} +``` diff --git a/packages/aws-cdk-lib/cx-api/lib/features.ts b/packages/aws-cdk-lib/cx-api/lib/features.ts index 8d0faf244f2f6..46c159ef4435c 100644 --- a/packages/aws-cdk-lib/cx-api/lib/features.ts +++ b/packages/aws-cdk-lib/cx-api/lib/features.ts @@ -107,6 +107,7 @@ export const EKS_NODEGROUP_NAME = '@aws-cdk/aws-eks:nodegroupNameAttribute'; export const EBS_DEFAULT_GP3 = '@aws-cdk/aws-ec2:ebsDefaultGp3Volume'; export const ECS_REMOVE_DEFAULT_DEPLOYMENT_ALARM = '@aws-cdk/aws-ecs:removeDefaultDeploymentAlarm'; export const LOG_API_RESPONSE_DATA_PROPERTY_TRUE_DEFAULT = '@aws-cdk/custom-resources:logApiResponseDataPropertyTrueDefault'; +export const CLOUDFRONT_USE_ORIGIN_ACCESS_CONTROL = '@aws-cdk/aws-cloudfront:useOriginAccessControl'; export const FLAGS: Record = { ////////////////////////////////////////////////////////////////////// @@ -1072,7 +1073,7 @@ export const FLAGS: Record = { type: FlagType.ApiDefault, summary: 'When enabled, the default volume type of the EBS volume will be GP3', detailsMd: ` - When this featuer flag is enabled, the default volume type of the EBS volume will be \`EbsDeviceVolumeType.GENERAL_PURPOSE_SSD_GP3\`. + When this feature flag is enabled, the default volume type of the EBS volume will be \`EbsDeviceVolumeType.GENERAL_PURPOSE_SSD_GP3\`. `, introducedIn: { v2: '2.140.0' }, recommendedValue: true, @@ -1121,6 +1122,18 @@ export const FLAGS: Record = { introducedIn: { v2: 'V2NEXT' }, recommendedValue: true, }, + + ////////////////////////////////////////////////////////////////////// + [CLOUDFRONT_USE_ORIGIN_ACCESS_CONTROL]: { + type: FlagType.BugFix, + summary: 'When enabled, an origin access control will be created automatically when a new S3 origin is created.', + detailsMd: ` + When this feature flag is enabled, an origin access control will be created automatically when a new \`S3Origin\` is created instead + of an origin access identity (legacy). + `, + introducedIn: { v2: 'V2NEXT' }, + recommendedValue: true, + }, }; const CURRENT_MV = 'v2'; From caf3cfb524b9e38958f013bf504c3908c16da73e Mon Sep 17 00:00:00 2001 From: gracelu0 Date: Fri, 24 May 2024 09:45:54 -0700 Subject: [PATCH 02/14] create custom resource to update kms policy --- .../index.ts | 95 ++++++++++++++++ .../lib/custom-resources-framework/config.ts | 8 ++ .../custom-resource-handlers/package.json | 2 +- .../aws-cloudfront-origins/lib/s3-origin.ts | 103 ++++++++++++------ .../lib/origin-access-control.ts | 12 -- 5 files changed, 172 insertions(+), 48 deletions(-) create mode 100644 packages/@aws-cdk/custom-resource-handlers/lib/aws-cloudfront-origins/s3-origin-access-control-policy-handler/index.ts diff --git a/packages/@aws-cdk/custom-resource-handlers/lib/aws-cloudfront-origins/s3-origin-access-control-policy-handler/index.ts b/packages/@aws-cdk/custom-resource-handlers/lib/aws-cloudfront-origins/s3-origin-access-control-policy-handler/index.ts new file mode 100644 index 0000000000000..229cb62b594a5 --- /dev/null +++ b/packages/@aws-cdk/custom-resource-handlers/lib/aws-cloudfront-origins/s3-origin-access-control-policy-handler/index.ts @@ -0,0 +1,95 @@ +/* eslint-disable import/no-extraneous-dependencies */ +import { KMS, KeyManagerType } from '@aws-sdk/client-kms'; + +const kmsClient = new KMS({}); + +export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent) { + + if (event.RequestType === 'Create' || event.RequestType === 'Update') { + const props = event.ResourceProperties; + const distributionId = props.DistributionId; + const kmsKeyId = props.KmsKeyId; + const accountId = props.AccountId; + const partition = props.Partition; + const region = process.env.AWS_REGION; + + const describeKeyCommandResponse = await kmsClient.describeKey({ + KeyId: kmsKeyId, + }); + + if (describeKeyCommandResponse.KeyMetadata?.KeyManager === KeyManagerType.AWS) { + // AWS managed key, cannot update key policy + return; + } + + // The PolicyName is specified as "default" below because that is the only valid name as + // written in the documentation for @aws-sdk/client-kms.GetKeyPolicyCommandInput: + // https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/Package/-aws-sdk-client-kms/Interface/GetKeyPolicyCommandInput/ + const getKeyPolicyCommandResponse = await kmsClient.getKeyPolicy({ + KeyId: kmsKeyId, + PolicyName: 'default', + }); + + if (!getKeyPolicyCommandResponse.Policy) { + throw new Error('An error occurred while retrieving the key policy.'); + } + + // Define the updated key policy to allow CloudFront Distribution access + const keyPolicy = JSON.parse(getKeyPolicyCommandResponse?.Policy); + const kmsKeyPolicyStatement = { + Sid: 'AllowCloudFrontServicePrincipalSSE-KMS', + Effect: 'Allow', + Principal: { + Service: [ + 'cloudfront.amazonaws.com', + ], + }, + Action: [ + 'kms:Decrypt', + 'kms:Encrypt', + 'kms:GenerateDataKey*', + ], + Resource: `arn:${partition}:kms:${region}:${accountId}:key/${kmsKeyId}`, + Condition: { + StringEquals: { + 'AWS:SourceArn': `arn:${partition}:cloudfront::${accountId}:distribution/${distributionId}`, + }, + }, + }; + const updatedKeyPolicy = updateKeyPolicy(keyPolicy, kmsKeyPolicyStatement); + await kmsClient.putKeyPolicy({ + KeyId: kmsKeyId, + Policy: JSON.stringify(updatedKeyPolicy), + PolicyName: 'default', + }); + + return { + IsComplete: true, + }; + } else if (event.RequestType === 'Delete') { + return; + } +} + +/** + * Updates a provided key policy with a provided key policy statement. First checks whether the provided key policy statement + * already exists. If an existing key policy is found with a matching sid, the provided key policy will overwrite the existing + * key policy. If no matching key policy is found, the provided key policy will be appended onto the array of policy statements. + * @param keyPolicy - the JSON.parse'd result of the otherwise stringified key policy. + * @param keyPolicyStatement - the key policy statement to be added to the key policy. + * @returns keyPolicy - the updated key policy. + */ +export const updateKeyPolicy = (keyPolicy: any, keyPolicyStatement: any) => { + // Check to see if a duplicate key policy exists by matching on the sid. This is to prevent duplicate key policies + // from being added/updated in response to a stack being updated one or more times after initial creation. + const existingKeyPolicyIndex = keyPolicy.Statement.findIndex((statement: any) => statement.Sid === keyPolicyStatement.Sid); + // If a match is found, overwrite the key policy statement... + // Otherwise, push the new key policy to the array of statements + if (existingKeyPolicyIndex > -1) { + keyPolicy.Statement[existingKeyPolicyIndex] = keyPolicyStatement; + } else { + keyPolicy.Statement.push(keyPolicyStatement); + } + // Return the result + return keyPolicy; +}; \ No newline at end of file diff --git a/packages/@aws-cdk/custom-resource-handlers/lib/custom-resources-framework/config.ts b/packages/@aws-cdk/custom-resource-handlers/lib/custom-resources-framework/config.ts index 41f18c6d6a1aa..959b5ad69f3cb 100644 --- a/packages/@aws-cdk/custom-resource-handlers/lib/custom-resources-framework/config.ts +++ b/packages/@aws-cdk/custom-resource-handlers/lib/custom-resources-framework/config.ts @@ -118,6 +118,14 @@ export const config: HandlerFrameworkConfig = { }, ], }, + 'aws-cloudfront-origins': { + 's3-origin-access-control-policy-provider': [ + { + type: ComponentType.CUSTOM_RESOURCE_PROVIDER, + sourceCode: path.resolve(__dirname, '..', 'aws-cloudfront-origins', 's3-origin-access-control-policy-handler', 'index.ts'), + }, + ], + }, 'aws-dynamodb': { 'replica-provider': [ { diff --git a/packages/@aws-cdk/custom-resource-handlers/package.json b/packages/@aws-cdk/custom-resource-handlers/package.json index 52cd46777b5b7..9685462800368 100644 --- a/packages/@aws-cdk/custom-resource-handlers/package.json +++ b/packages/@aws-cdk/custom-resource-handlers/package.json @@ -31,7 +31,6 @@ "@aws-sdk/client-ecs": "3.451.0", "@aws-sdk/client-ssm": "3.453.0", "@aws-sdk/client-kinesis": "3.451.0", - "@aws-sdk/client-kms": "3.451.0", "@aws-sdk/client-codepipeline": "3.451.0", "@aws-sdk/client-redshift": "3.452.0", "@aws-sdk/client-account": "3.451.0", @@ -63,6 +62,7 @@ "@aws-sdk/client-lambda": "3.421.0", "@aws-sdk/client-synthetics": "3.421.0", "@aws-sdk/client-ecr": "3.421.0", + "@aws-sdk/client-kms": "3.451.0", "@aws-sdk/client-s3": "3.421.0", "@aws-sdk/client-cloudwatch": "3.421.0" }, diff --git a/packages/aws-cdk-lib/aws-cloudfront-origins/lib/s3-origin.ts b/packages/aws-cdk-lib/aws-cloudfront-origins/lib/s3-origin.ts index edf4b37c15af5..b15cace590bb0 100644 --- a/packages/aws-cdk-lib/aws-cloudfront-origins/lib/s3-origin.ts +++ b/packages/aws-cdk-lib/aws-cloudfront-origins/lib/s3-origin.ts @@ -3,9 +3,12 @@ import { HttpOrigin } from './http-origin'; import * as cloudfront from '../../aws-cloudfront'; import * as iam from '../../aws-iam'; import * as s3 from '../../aws-s3'; -import { Stack, Names, FeatureFlags, Aws, Lazy } from '../../core'; +import { Stack, Names, FeatureFlags, Aws, Lazy, CustomResource } from '../../core'; +import { S3OriginAccessControlPolicyProvider } from '../../custom-resource-handlers/dist/aws-cloudfront-origins/s3-origin-access-control-policy-provider.generated'; import * as cxapi from '../../cx-api'; +const S3_ORIGIN_ACCESS_CONTROL_RESOURCE_TYPE = 'Custom::S3OriginAccessControlPolicy'; + /** * Properties to use to customize an S3 Origin. */ @@ -99,45 +102,75 @@ class S3BucketOrigin extends cloudfront.OriginBase { this.originAccessIdentity = props.originAccessIdentity; } - public bind(scope: Construct, options: cloudfront.OriginBindOptions): cloudfront.OriginBindConfig { - if (FeatureFlags.of(scope).isEnabled(cxapi.CLOUDFRONT_USE_ORIGIN_ACCESS_CONTROL)) { - if (!this.originAccessControl) { - // Create a new origin access control if not specified - this.originAccessControl = new cloudfront.OriginAccessControl(scope, 'S3OriginAccessControl'); - } - const distribution = scope.node.scope as cloudfront.Distribution; - const distributionId = Lazy.string({ produce: () => distribution.distributionId }); - const oacReadOnlyBucketPolicyStatement = new iam.PolicyStatement( - { - sid: 'AllowS3OACAccess', - effect: iam.Effect.ALLOW, - principals: [new iam.ServicePrincipal('cloudfront.amazonaws.com')], - actions: ['s3:GetObject'], - resources: [this.bucket.arnForObjects('*')], - conditions: { - StringEquals: { - // eslint-disable-next-line @aws-cdk/no-literal-partition - 'AWS:SourceArn': `arn:aws:cloudfront::${Aws.ACCOUNT_ID}:distribution/${distributionId}`, - }, + private bindOAC(scope: Construct, options: cloudfront.OriginBindOptions): cloudfront.OriginBindConfig { + if (!this.originAccessControl) { + // Create a new origin access control if not specified + this.originAccessControl = new cloudfront.OriginAccessControl(scope, 'S3OriginAccessControl'); + } + const distribution = scope.node.scope as cloudfront.Distribution; + const distributionId = Lazy.string({ produce: () => distribution.distributionId }); + const oacReadOnlyBucketPolicyStatement = new iam.PolicyStatement( + { + sid: 'AllowS3OACAccess', + effect: iam.Effect.ALLOW, + principals: [new iam.ServicePrincipal('cloudfront.amazonaws.com')], + actions: ['s3:GetObject'], + resources: [this.bucket.arnForObjects('*')], + conditions: { + StringEquals: { + 'AWS:SourceArn': `arn:${Aws.PARTITION}:cloudfront::${Aws.ACCOUNT_ID}:distribution/${distributionId}`, }, }, - ); - const result = this.bucket.addToResourcePolicy(oacReadOnlyBucketPolicyStatement); + }, + ); + const result = this.bucket.addToResourcePolicy(oacReadOnlyBucketPolicyStatement); - if (!result.statementAdded) { - throw new Error('Policy statement was not added to bucket policy'); - } + if (!result.statementAdded) { + throw new Error('Policy statement was not added to bucket policy'); + } - const originBindConfig = super.bind(scope, options); + if (this.bucket.encryptionKey) { + const provider = S3OriginAccessControlPolicyProvider.getOrCreateProvider(scope, S3_ORIGIN_ACCESS_CONTROL_RESOURCE_TYPE, + { + description: 'Lambda function that updates SSE-KMS key policy to allow CloudFront distribution access.', + }); + provider.addToRolePolicy({ + Action: ['kms:PutKeyPolicy', 'kms:GetKeyPolicy', 'kms:DescribeKey'], + Effect: 'Allow', + Resource: [this.bucket.encryptionKey.keyArn], + }); - // Update configuration to set OriginControlAccessId property - return { - ...originBindConfig, - originProperty: { - ...originBindConfig.originProperty!, - originAccessControlId: this.originAccessControl.originAccessControlId, + new CustomResource(scope, 'S3OriginKMSKeyPolicyCustomResource', { + resourceType: S3_ORIGIN_ACCESS_CONTROL_RESOURCE_TYPE, + serviceToken: provider.serviceToken, + properties: { + DistributionId: distributionId, + KmsKeyId: this.bucket.encryptionKey.keyId, + AccountId: this.bucket.env.account, + Partition: Stack.of(scope).partition, }, - }; + }); + } + + const originBindConfig = super.bind(scope, options); + + // Update configuration to set OriginControlAccessId property + return { + ...originBindConfig, + originProperty: { + ...originBindConfig.originProperty!, + originAccessControlId: this.originAccessControl.originAccessControlId, + }, + }; + } + + // private giveOACPermissions() { + // const resourceType = 'Custom::S3OriginOACPolicyConfiguration'; + // } + + public bind(scope: Construct, options: cloudfront.OriginBindOptions): cloudfront.OriginBindConfig { + if (this.originAccessControl || FeatureFlags.of(scope).isEnabled(cxapi.CLOUDFRONT_USE_ORIGIN_ACCESS_CONTROL)) { + return this.bindOAC(scope, options); } else if (!this.originAccessIdentity && !(FeatureFlags.of(scope).isEnabled(cxapi.CLOUDFRONT_USE_ORIGIN_ACCESS_CONTROL))) { // Using a bucket from another stack creates a cyclic reference with // the bucket taking a dependency on the generated S3CanonicalUserId for the grant principal, @@ -161,7 +194,7 @@ class S3BucketOrigin extends cloudfront.OriginBase { actions: ['s3:GetObject'], principals: [this.originAccessIdentity.grantPrincipal], })); - } + }; return super.bind(scope, options); } diff --git a/packages/aws-cdk-lib/aws-cloudfront/lib/origin-access-control.ts b/packages/aws-cdk-lib/aws-cloudfront/lib/origin-access-control.ts index b59b3d738640b..21ef2b4905d08 100644 --- a/packages/aws-cdk-lib/aws-cloudfront/lib/origin-access-control.ts +++ b/packages/aws-cdk-lib/aws-cloudfront/lib/origin-access-control.ts @@ -11,17 +11,10 @@ export interface IOriginAccessControl extends IResource { * @attribute */ readonly originAccessControlId: string; - - /** - * The name of the origin access control. - * @attribute - */ - readonly originAccessControlName: string; } abstract class OriginAccessControlBase extends Resource implements IOriginAccessControl { public abstract readonly originAccessControlId: string; - public abstract readonly originAccessControlName: string; } /** @@ -122,12 +115,10 @@ export class OriginAccessControl extends OriginAccessControlBase { public static fromOriginAccessControlId(scope: Construct, id: string, originAccessControlId: string): IOriginAccessControl { class Import extends OriginAccessControlBase { public readonly originAccessControlId = originAccessControlId; - public readonly originAccessControlName = originAccessControlId; constructor(s: Construct, i: string) { super(s, i); this.originAccessControlId = originAccessControlId; - this.originAccessControlName = originAccessControlId; } } return new Import(scope, id); @@ -177,8 +168,6 @@ export class OriginAccessControl extends OriginAccessControlBase { } public readonly originAccessControlId: string; - public readonly originAccessControlName: string; - constructor(scope: Construct, id: string, props: OriginAccessControlProps = {}) { super(scope, id); @@ -193,7 +182,6 @@ export class OriginAccessControl extends OriginAccessControlBase { }); this.originAccessControlId = resource.attrId; - this.originAccessControlName = resource.ref; } private generateName(): string { From 166a8983a4bfc5bc714e064882a6ec2048a32356 Mon Sep 17 00:00:00 2001 From: gracelu0 Date: Tue, 28 May 2024 01:59:14 -0700 Subject: [PATCH 03/14] custom resource for bucket policy --- .../index.ts | 122 ++++++++++++++++++ .../index.ts | 37 +++--- .../lib/custom-resources-framework/config.ts | 10 +- .../aws-cloudfront-origins/lib/s3-origin.ts | 47 ++++--- 4 files changed, 180 insertions(+), 36 deletions(-) create mode 100644 packages/@aws-cdk/custom-resource-handlers/lib/aws-cloudfront-origins/s3-origin-access-control-bucket-policy-handler/index.ts rename packages/@aws-cdk/custom-resource-handlers/lib/aws-cloudfront-origins/{s3-origin-access-control-policy-handler => s3-origin-access-control-key-policy-handler}/index.ts (65%) diff --git a/packages/@aws-cdk/custom-resource-handlers/lib/aws-cloudfront-origins/s3-origin-access-control-bucket-policy-handler/index.ts b/packages/@aws-cdk/custom-resource-handlers/lib/aws-cloudfront-origins/s3-origin-access-control-bucket-policy-handler/index.ts new file mode 100644 index 0000000000000..c0e8d162a3ba7 --- /dev/null +++ b/packages/@aws-cdk/custom-resource-handlers/lib/aws-cloudfront-origins/s3-origin-access-control-bucket-policy-handler/index.ts @@ -0,0 +1,122 @@ +/* eslint-disable no-console */ +/* eslint-disable import/no-extraneous-dependencies */ +import { S3 } from '@aws-sdk/client-s3'; + +const S3_POLICY_STUB = JSON.stringify({ Version: '2012-10-17', Statement: [] }); + +const s3 = new S3({}); + +interface updateBucketPolicyProps { + bucketName: string; + distributionId: string; + partition: string; + accountId: string; +} + +export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent) { + + if (event.RequestType === 'Create' || event.RequestType === 'Update') { + const props = event.ResourceProperties; + const distributionId = props.DistributionId; + const accountId = props.AccountId; + const partition = props.Partition; + const bucketName = props.BucketName; + const isImportedBucket = props.IsImportedBucket; + + if (isImportedBucket) { + await updateBucketPolicy({ bucketName, distributionId, partition, accountId }); + } + + return { + IsComplete: true, + }; + } else { + return; + } +} + +async function updateBucketPolicy(props: updateBucketPolicyProps) { + // make API calls to update bucket policy + try { + console.log('calling getBucketPolicy...'); + const prevPolicyJson = (await s3.getBucketPolicy({ Bucket: props.bucketName }))?.Policy ?? S3_POLICY_STUB; + const policy = JSON.parse(prevPolicyJson); + console.log('Previous bucket policy:', JSON.stringify(policy, undefined, 2)); + const oacBucketPolicyStatement = { + Sid: 'AllowS3OACAccess', + Principal: { + Service: ['cloudfront.amazonaws.com'], + }, + Effect: 'Allow', + Action: ['s3:GetObject'], + Resource: [`arn:${props.partition}:s3:::${props.bucketName}/*`], + Condition: { + StringEquals: { + 'AWS:SourceArn': `arn:${props.partition}:cloudfront::${props.accountId}:distribution/${props.distributionId}`, + }, + }, + }; + // Give Origin Access Control permission to access the bucket + let updatedBucketPolicy = updatePolicy(policy, oacBucketPolicyStatement); + console.log('Updated bucket policy', JSON.stringify(updatedBucketPolicy, undefined, 2)); + + await s3.putBucketPolicy({ + Bucket: props.bucketName, + Policy: JSON.stringify(updatedBucketPolicy), + }); + console.log('AFTER FIRST: Put bucket policy'); + + // Check if policy has OAI principal and remove + updatedBucketPolicy.Statement = updatedBucketPolicy.Statement.filter((statement: any) => !isOaiPrincipal(statement)); + console.log('Updated bucket policy AGAIN:', JSON.stringify(updatedBucketPolicy, undefined, 2)); + await s3.putBucketPolicy({ + Bucket: props.bucketName, + Policy: JSON.stringify(updatedBucketPolicy), + }); + + console.log('success!'); + } catch (error: any) { + console.log(error); + if (error.name === 'NoSuchBucket') { + throw error; // Rethrow for further logging/handling up the stack + } + + console.log(`Could not set new origin access control policy on bucket '${props.bucketName}'.`); + } +} + +/** + * Updates a provided policy with a provided policy statement. First checks whether the provided policy statement + * already exists. If an existing policy is found with a matching sid, the provided policy will overwrite the existing + * policy. If no matching policy is found, the provided policy will be appended onto the array of policy statements. + * @param currentPolicy - the JSON.parse'd result of the otherwise stringified policy. + * @param policyStatementToAdd - the policy statement to be added to the policy. + * @returns currentPolicy - the updated policy. + */ +function updatePolicy(currentPolicy: any, policyStatementToAdd: any) { + // Check to see if a duplicate key policy exists by matching on the sid. This is to prevent duplicate key policies + // from being added/updated in response to a stack being updated one or more times after initial creation. + const existingPolicyIndex = currentPolicy.Statement.findIndex((statement: any) => statement.Sid === policyStatementToAdd.Sid); + // If a match is found, overwrite the key policy statement... + // Otherwise, push the new key policy to the array of statements + if (existingPolicyIndex > -1) { + currentPolicy.Statement[existingPolicyIndex] = policyStatementToAdd; + } else { + currentPolicy.Statement.push(policyStatementToAdd); + } + // Return the result + return currentPolicy; +}; + +/** + * Check if the policy contains an OAI principal + */ +function isOaiPrincipal(statement: any) { + if (statement.Principal && statement.Principal.AWS) { + const principal = statement.Principal.AWS; + if (typeof principal === 'string' && principal.includes('cloudfront:user/CloudFront Origin Access Identity')) { + return true; + } + } + return false; +} \ No newline at end of file diff --git a/packages/@aws-cdk/custom-resource-handlers/lib/aws-cloudfront-origins/s3-origin-access-control-policy-handler/index.ts b/packages/@aws-cdk/custom-resource-handlers/lib/aws-cloudfront-origins/s3-origin-access-control-key-policy-handler/index.ts similarity index 65% rename from packages/@aws-cdk/custom-resource-handlers/lib/aws-cloudfront-origins/s3-origin-access-control-policy-handler/index.ts rename to packages/@aws-cdk/custom-resource-handlers/lib/aws-cloudfront-origins/s3-origin-access-control-key-policy-handler/index.ts index 229cb62b594a5..a77da0bc38c60 100644 --- a/packages/@aws-cdk/custom-resource-handlers/lib/aws-cloudfront-origins/s3-origin-access-control-policy-handler/index.ts +++ b/packages/@aws-cdk/custom-resource-handlers/lib/aws-cloudfront-origins/s3-origin-access-control-key-policy-handler/index.ts @@ -1,7 +1,8 @@ +/* eslint-disable no-console */ /* eslint-disable import/no-extraneous-dependencies */ import { KMS, KeyManagerType } from '@aws-sdk/client-kms'; -const kmsClient = new KMS({}); +const kms = new KMS({}); export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent) { @@ -13,7 +14,7 @@ export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent const partition = props.Partition; const region = process.env.AWS_REGION; - const describeKeyCommandResponse = await kmsClient.describeKey({ + const describeKeyCommandResponse = await kms.describeKey({ KeyId: kmsKeyId, }); @@ -25,7 +26,7 @@ export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent // The PolicyName is specified as "default" below because that is the only valid name as // written in the documentation for @aws-sdk/client-kms.GetKeyPolicyCommandInput: // https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/Package/-aws-sdk-client-kms/Interface/GetKeyPolicyCommandInput/ - const getKeyPolicyCommandResponse = await kmsClient.getKeyPolicy({ + const getKeyPolicyCommandResponse = await kms.getKeyPolicy({ KeyId: kmsKeyId, PolicyName: 'default', }); @@ -56,8 +57,8 @@ export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent }, }, }; - const updatedKeyPolicy = updateKeyPolicy(keyPolicy, kmsKeyPolicyStatement); - await kmsClient.putKeyPolicy({ + const updatedKeyPolicy = updatePolicy(keyPolicy, kmsKeyPolicyStatement); + await kms.putKeyPolicy({ KeyId: kmsKeyId, Policy: JSON.stringify(updatedKeyPolicy), PolicyName: 'default', @@ -66,30 +67,30 @@ export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent return { IsComplete: true, }; - } else if (event.RequestType === 'Delete') { + } else { return; } } /** - * Updates a provided key policy with a provided key policy statement. First checks whether the provided key policy statement - * already exists. If an existing key policy is found with a matching sid, the provided key policy will overwrite the existing - * key policy. If no matching key policy is found, the provided key policy will be appended onto the array of policy statements. - * @param keyPolicy - the JSON.parse'd result of the otherwise stringified key policy. - * @param keyPolicyStatement - the key policy statement to be added to the key policy. - * @returns keyPolicy - the updated key policy. + * Updates a provided policy with a provided policy statement. First checks whether the provided policy statement + * already exists. If an existing policy is found with a matching sid, the provided policy will overwrite the existing + * policy. If no matching policy is found, the provided policy will be appended onto the array of policy statements. + * @param currentPolicy - the JSON.parse'd result of the otherwise stringified policy. + * @param policyStatementToAdd - the policy statement to be added to the policy. + * @returns currentPolicy - the updated policy. */ -export const updateKeyPolicy = (keyPolicy: any, keyPolicyStatement: any) => { +export const updatePolicy = (currentPolicy: any, policyStatementToAdd: any) => { // Check to see if a duplicate key policy exists by matching on the sid. This is to prevent duplicate key policies // from being added/updated in response to a stack being updated one or more times after initial creation. - const existingKeyPolicyIndex = keyPolicy.Statement.findIndex((statement: any) => statement.Sid === keyPolicyStatement.Sid); + const existingPolicyIndex = currentPolicy.Statement.findIndex((statement: any) => statement.Sid === policyStatementToAdd.Sid); // If a match is found, overwrite the key policy statement... // Otherwise, push the new key policy to the array of statements - if (existingKeyPolicyIndex > -1) { - keyPolicy.Statement[existingKeyPolicyIndex] = keyPolicyStatement; + if (existingPolicyIndex > -1) { + currentPolicy.Statement[existingPolicyIndex] = policyStatementToAdd; } else { - keyPolicy.Statement.push(keyPolicyStatement); + currentPolicy.Statement.push(policyStatementToAdd); } // Return the result - return keyPolicy; + return currentPolicy; }; \ No newline at end of file diff --git a/packages/@aws-cdk/custom-resource-handlers/lib/custom-resources-framework/config.ts b/packages/@aws-cdk/custom-resource-handlers/lib/custom-resources-framework/config.ts index 959b5ad69f3cb..74c17fb505009 100644 --- a/packages/@aws-cdk/custom-resource-handlers/lib/custom-resources-framework/config.ts +++ b/packages/@aws-cdk/custom-resource-handlers/lib/custom-resources-framework/config.ts @@ -119,10 +119,16 @@ export const config: HandlerFrameworkConfig = { ], }, 'aws-cloudfront-origins': { - 's3-origin-access-control-policy-provider': [ + 's3-origin-access-control-key-policy-provider': [ { type: ComponentType.CUSTOM_RESOURCE_PROVIDER, - sourceCode: path.resolve(__dirname, '..', 'aws-cloudfront-origins', 's3-origin-access-control-policy-handler', 'index.ts'), + sourceCode: path.resolve(__dirname, '..', 'aws-cloudfront-origins', 's3-origin-access-control-key-policy-handler', 'index.ts'), + }, + ], + 's3-origin-access-control-bucket-policy-provider': [ + { + type: ComponentType.CUSTOM_RESOURCE_PROVIDER, + sourceCode: path.resolve(__dirname, '..', 'aws-cloudfront-origins', 's3-origin-access-control-bucket-policy-handler', 'index.ts'), }, ], }, diff --git a/packages/aws-cdk-lib/aws-cloudfront-origins/lib/s3-origin.ts b/packages/aws-cdk-lib/aws-cloudfront-origins/lib/s3-origin.ts index b15cace590bb0..24c625c2dae10 100644 --- a/packages/aws-cdk-lib/aws-cloudfront-origins/lib/s3-origin.ts +++ b/packages/aws-cdk-lib/aws-cloudfront-origins/lib/s3-origin.ts @@ -3,12 +3,13 @@ import { HttpOrigin } from './http-origin'; import * as cloudfront from '../../aws-cloudfront'; import * as iam from '../../aws-iam'; import * as s3 from '../../aws-s3'; -import { Stack, Names, FeatureFlags, Aws, Lazy, CustomResource } from '../../core'; -import { S3OriginAccessControlPolicyProvider } from '../../custom-resource-handlers/dist/aws-cloudfront-origins/s3-origin-access-control-policy-provider.generated'; +import { Stack, Names, FeatureFlags, Aws, Lazy, CustomResource, Annotations } from '../../core'; +import { S3OriginAccessControlBucketPolicyProvider } from '../../custom-resource-handlers/dist/aws-cloudfront-origins/s3-origin-access-control-bucket-policy-provider.generated'; +import { S3OriginAccessControlKeyPolicyProvider } from '../../custom-resource-handlers/dist/aws-cloudfront-origins/s3-origin-access-control-key-policy-provider.generated'; import * as cxapi from '../../cx-api'; -const S3_ORIGIN_ACCESS_CONTROL_RESOURCE_TYPE = 'Custom::S3OriginAccessControlPolicy'; - +const S3_ORIGIN_ACCESS_CONTROL_KEY_RESOURCE_TYPE = 'Custom::S3OriginAccessControlKeyPolicy'; +const S3_ORIGIN_ACCESS_CONTROL_BUCKET_RESOURCE_TYPE = 'Custom::S3OriginAccessControlBucketPolicy'; /** * Properties to use to customize an S3 Origin. */ @@ -92,9 +93,6 @@ class S3BucketOrigin extends cloudfront.OriginBase { constructor(private readonly bucket: s3.IBucket, props: S3OriginProps) { super(bucket.bucketRegionalDomainName, props); - // if (originAccessIdentity) { - // this.originAccessIdentity = originAccessIdentity; - // } if (props.originAccessControl && props.originAccessIdentity) { throw new Error('Only one of originAccessControl or originAccessIdentity can be specified for an origin.'); } @@ -107,8 +105,7 @@ class S3BucketOrigin extends cloudfront.OriginBase { // Create a new origin access control if not specified this.originAccessControl = new cloudfront.OriginAccessControl(scope, 'S3OriginAccessControl'); } - const distribution = scope.node.scope as cloudfront.Distribution; - const distributionId = Lazy.string({ produce: () => distribution.distributionId }); + const distributionId = options.distributionId; const oacReadOnlyBucketPolicyStatement = new iam.PolicyStatement( { sid: 'AllowS3OACAccess', @@ -125,12 +122,34 @@ class S3BucketOrigin extends cloudfront.OriginBase { ); const result = this.bucket.addToResourcePolicy(oacReadOnlyBucketPolicyStatement); + // Failed to update bucket policy, assume using imported bucket if (!result.statementAdded) { - throw new Error('Policy statement was not added to bucket policy'); + Annotations.of(scope).addWarningV2('@aws-cdk/aws-cloudfront-origins:updateBucketPolicy', 'Cannot update bucket policy of an imported bucket. Update the policy manually instead.'); + const provider = S3OriginAccessControlBucketPolicyProvider.getOrCreateProvider(scope, S3_ORIGIN_ACCESS_CONTROL_BUCKET_RESOURCE_TYPE, + { + description: 'Lambda function that updates S3 bucket policy to allow CloudFront distribution access.', + }); + provider.addToRolePolicy({ + Action: ['s3:getBucketPolicy', 's3:putBucketPolicy'], + Effect: 'Allow', + Resource: [this.bucket.bucketArn], + }); + + new CustomResource(scope, 'S3OriginBucketPolicyCustomResource', { + resourceType: S3_ORIGIN_ACCESS_CONTROL_BUCKET_RESOURCE_TYPE, + serviceToken: provider.serviceToken, + properties: { + DistributionId: distributionId, + AccountId: this.bucket.env.account, + Partition: Stack.of(scope).partition, + BucketName: this.bucket.bucketName, + IsImportedBucket: !result.statementAdded, + }, + }); } if (this.bucket.encryptionKey) { - const provider = S3OriginAccessControlPolicyProvider.getOrCreateProvider(scope, S3_ORIGIN_ACCESS_CONTROL_RESOURCE_TYPE, + const provider = S3OriginAccessControlKeyPolicyProvider.getOrCreateProvider(scope, S3_ORIGIN_ACCESS_CONTROL_KEY_RESOURCE_TYPE, { description: 'Lambda function that updates SSE-KMS key policy to allow CloudFront distribution access.', }); @@ -141,7 +160,7 @@ class S3BucketOrigin extends cloudfront.OriginBase { }); new CustomResource(scope, 'S3OriginKMSKeyPolicyCustomResource', { - resourceType: S3_ORIGIN_ACCESS_CONTROL_RESOURCE_TYPE, + resourceType: S3_ORIGIN_ACCESS_CONTROL_KEY_RESOURCE_TYPE, serviceToken: provider.serviceToken, properties: { DistributionId: distributionId, @@ -164,10 +183,6 @@ class S3BucketOrigin extends cloudfront.OriginBase { }; } - // private giveOACPermissions() { - // const resourceType = 'Custom::S3OriginOACPolicyConfiguration'; - // } - public bind(scope: Construct, options: cloudfront.OriginBindOptions): cloudfront.OriginBindConfig { if (this.originAccessControl || FeatureFlags.of(scope).isEnabled(cxapi.CLOUDFRONT_USE_ORIGIN_ACCESS_CONTROL)) { return this.bindOAC(scope, options); From 00133e99f2203f87b27c92ba35e3e0d404159e7e Mon Sep 17 00:00:00 2001 From: gracelu0 Date: Tue, 28 May 2024 10:38:16 -0700 Subject: [PATCH 04/14] Support oac in webDistribution --- .../index.ts | 5 ++- .../aws-cloudfront/lib/web-distribution.ts | 36 +++++++++++++++++++ 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/packages/@aws-cdk/custom-resource-handlers/lib/aws-cloudfront-origins/s3-origin-access-control-bucket-policy-handler/index.ts b/packages/@aws-cdk/custom-resource-handlers/lib/aws-cloudfront-origins/s3-origin-access-control-bucket-policy-handler/index.ts index c0e8d162a3ba7..3f7f357366c76 100644 --- a/packages/@aws-cdk/custom-resource-handlers/lib/aws-cloudfront-origins/s3-origin-access-control-bucket-policy-handler/index.ts +++ b/packages/@aws-cdk/custom-resource-handlers/lib/aws-cloudfront-origins/s3-origin-access-control-bucket-policy-handler/index.ts @@ -64,17 +64,16 @@ async function updateBucketPolicy(props: updateBucketPolicyProps) { Bucket: props.bucketName, Policy: JSON.stringify(updatedBucketPolicy), }); - console.log('AFTER FIRST: Put bucket policy'); // Check if policy has OAI principal and remove updatedBucketPolicy.Statement = updatedBucketPolicy.Statement.filter((statement: any) => !isOaiPrincipal(statement)); - console.log('Updated bucket policy AGAIN:', JSON.stringify(updatedBucketPolicy, undefined, 2)); + await s3.putBucketPolicy({ Bucket: props.bucketName, Policy: JSON.stringify(updatedBucketPolicy), }); - console.log('success!'); + console.log('Updated bucket policy to remove OAI principal policy statement:', JSON.stringify(updatedBucketPolicy, undefined, 2)); } catch (error: any) { console.log(error); if (error.name === 'NoSuchBucket') { diff --git a/packages/aws-cdk-lib/aws-cloudfront/lib/web-distribution.ts b/packages/aws-cdk-lib/aws-cloudfront/lib/web-distribution.ts index dcb120fe3bc26..ba0fa61da0bba 100644 --- a/packages/aws-cdk-lib/aws-cloudfront/lib/web-distribution.ts +++ b/packages/aws-cdk-lib/aws-cloudfront/lib/web-distribution.ts @@ -12,6 +12,7 @@ import * as iam from '../../aws-iam'; import * as lambda from '../../aws-lambda'; import * as s3 from '../../aws-s3'; import * as cdk from '../../core'; +import { Annotations } from '../../core'; /** * HTTP status code to failover to second origin @@ -318,6 +319,13 @@ export interface S3OriginConfig { */ readonly originAccessIdentity?: IOriginAccessIdentity; + /** + * The optional Origin Access Control that Cloudfront will use when accessing your S3 bucket. + * + * @default - No origin access control + */ + readonly originAccessControl?: IOriginAccessControl; + /** * The relative path to the origin root to use for sources. * @@ -1125,6 +1133,9 @@ export class CloudFrontWebDistribution extends cdk.Resource implements IDistribu let s3OriginConfig: CfnDistribution.S3OriginConfigProperty | undefined; if (originConfig.s3OriginSource) { + if (originConfig.s3OriginSource.originAccessIdentity && originConfig.s3OriginSource.originAccessControl) { + throw Error('Only one of origin access identity or origin access control can be defined.'); + } // first case for backwards compatibility if (originConfig.s3OriginSource.originAccessIdentity) { // grant CloudFront OriginAccessIdentity read access to S3 bucket @@ -1141,6 +1152,30 @@ export class CloudFrontWebDistribution extends cdk.Resource implements IDistribu s3OriginConfig = { originAccessIdentity: `origin-access-identity/cloudfront/${originConfig.s3OriginSource.originAccessIdentity.originAccessIdentityId}`, }; + } else if (originConfig.s3OriginSource.originAccessControl) { + const oacReadOnlyBucketPolicyStatement = new iam.PolicyStatement( + { + sid: 'AllowS3OACAccess', + effect: iam.Effect.ALLOW, + principals: [new iam.ServicePrincipal('cloudfront.amazonaws.com')], + actions: ['s3:GetObject'], + resources: [originConfig.s3OriginSource.s3BucketSource.arnForObjects('*')], + conditions: { + StringEquals: { + 'AWS:SourceArn': formatDistributionArn(this), + }, + }, + }, + ); + const result = originConfig.s3OriginSource.s3BucketSource.addToResourcePolicy(oacReadOnlyBucketPolicyStatement); + + if (!result.statementAdded) { + Annotations.of(this).addWarningV2('@aws-cdk/aws-cloudfront:webDistribution', 'Cannot update bucket policy of an imported bucket. Update the policy manually instead.'); + } + + s3OriginConfig = { + originAccessIdentity: '', + }; } else { s3OriginConfig = {}; } @@ -1165,6 +1200,7 @@ export class CloudFrontWebDistribution extends cdk.Resource implements IDistribu originCustomHeaders: originHeaders.length > 0 ? originHeaders : undefined, s3OriginConfig, + originAccessControlId: originConfig.s3OriginSource?.originAccessControl?.originAccessControlId, originShield: this.toOriginShieldProperty(originConfig), customOriginConfig: originConfig.customOriginSource ? { From def44113e33f0a6f2257c75a5834aaa335a0fafd Mon Sep 17 00:00:00 2001 From: gracelu0 Date: Wed, 29 May 2024 15:05:22 -0700 Subject: [PATCH 05/14] refactor --- .../aws-cloudfront-origins/lib/s3-origin.ts | 85 +++++++++---------- .../lib/origin-access-control.ts | 43 ---------- 2 files changed, 40 insertions(+), 88 deletions(-) diff --git a/packages/aws-cdk-lib/aws-cloudfront-origins/lib/s3-origin.ts b/packages/aws-cdk-lib/aws-cloudfront-origins/lib/s3-origin.ts index 24c625c2dae10..8fe8ffb58b0a5 100644 --- a/packages/aws-cdk-lib/aws-cloudfront-origins/lib/s3-origin.ts +++ b/packages/aws-cdk-lib/aws-cloudfront-origins/lib/s3-origin.ts @@ -3,7 +3,7 @@ import { HttpOrigin } from './http-origin'; import * as cloudfront from '../../aws-cloudfront'; import * as iam from '../../aws-iam'; import * as s3 from '../../aws-s3'; -import { Stack, Names, FeatureFlags, Aws, Lazy, CustomResource, Annotations } from '../../core'; +import { Stack, Names, FeatureFlags, Aws, CustomResource, Annotations } from '../../core'; import { S3OriginAccessControlBucketPolicyProvider } from '../../custom-resource-handlers/dist/aws-cloudfront-origins/s3-origin-access-control-bucket-policy-provider.generated'; import { S3OriginAccessControlKeyPolicyProvider } from '../../custom-resource-handlers/dist/aws-cloudfront-origins/s3-origin-access-control-key-policy-provider.generated'; import * as cxapi from '../../cx-api'; @@ -39,11 +39,20 @@ export class S3Origin implements cloudfront.IOrigin { private readonly origin: cloudfront.IOrigin; constructor(bucket: s3.IBucket, props: S3OriginProps = {}) { - this.origin = bucket.isWebsite ? - new HttpOrigin(bucket.bucketWebsiteDomainName, { + if (props.originAccessControl && props.originAccessIdentity) { + throw new Error('Only one of originAccessControl or originAccessIdentity can be specified for an origin.'); + } + + if (bucket.isWebsite) { + this.origin = new HttpOrigin(bucket.bucketWebsiteDomainName, { protocolPolicy: cloudfront.OriginProtocolPolicy.HTTP_ONLY, // S3 only supports HTTP for website buckets ...props, - }) : new S3BucketOrigin(bucket, props); + }); + } else if (props.originAccessIdentity || !FeatureFlags.of(bucket.stack).isEnabled(cxapi.CLOUDFRONT_USE_ORIGIN_ACCESS_CONTROL)) { + this.origin = new S3BucketOrigin(bucket, props); + } else { + this.origin = new S3BucketOacOrigin(bucket, props); + } } public bind(scope: Construct, options: cloudfront.OriginBindOptions): cloudfront.OriginBindConfig { @@ -67,40 +76,6 @@ class S3BucketOacOrigin extends cloudfront.OriginBase { } public bind(scope: Construct, options: cloudfront.OriginBindOptions): cloudfront.OriginBindConfig { - if (!this.originAccessControl) { - this.originAccessControl = new cloudfront.OriginAccessControl(scope, options.originId); - } - return super.bind(scope, options); - } - - /** - * If you're using origin access control (OAC) instead of origin access identity, specify an empty `OriginAccessIdentity` element. - * @see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cloudfront-distribution-s3originconfig.html#cfn-cloudfront-distribution-s3originconfig-originaccessidentity - */ - protected renderS3OriginConfig(): cloudfront.CfnDistribution.S3OriginConfigProperty | undefined { - return { originAccessIdentity: '' }; - } -} - -/** - * An Origin specific to a S3 bucket (not configured for website hosting). - * - * Contains additional logic around bucket permissions and origin access identities. - */ -class S3BucketOrigin extends cloudfront.OriginBase { - private originAccessIdentity?: cloudfront.IOriginAccessIdentity; - private originAccessControl?: cloudfront.IOriginAccessControl; - - constructor(private readonly bucket: s3.IBucket, props: S3OriginProps) { - super(bucket.bucketRegionalDomainName, props); - if (props.originAccessControl && props.originAccessIdentity) { - throw new Error('Only one of originAccessControl or originAccessIdentity can be specified for an origin.'); - } - this.originAccessControl = props.originAccessControl; - this.originAccessIdentity = props.originAccessIdentity; - } - - private bindOAC(scope: Construct, options: cloudfront.OriginBindOptions): cloudfront.OriginBindConfig { if (!this.originAccessControl) { // Create a new origin access control if not specified this.originAccessControl = new cloudfront.OriginAccessControl(scope, 'S3OriginAccessControl'); @@ -183,10 +158,33 @@ class S3BucketOrigin extends cloudfront.OriginBase { }; } + /** + * If you're using origin access control (OAC) instead of origin access identity, specify an empty `OriginAccessIdentity` element. + * @see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cloudfront-distribution-s3originconfig.html#cfn-cloudfront-distribution-s3originconfig-originaccessidentity + */ + protected renderS3OriginConfig(): cloudfront.CfnDistribution.S3OriginConfigProperty | undefined { + return { originAccessIdentity: '' }; + } +} + +/** + * An Origin specific to a S3 bucket (not configured for website hosting). + * + * Contains additional logic around bucket permissions and origin access identities. + */ +class S3BucketOrigin extends cloudfront.OriginBase { + private originAccessIdentity!: cloudfront.IOriginAccessIdentity; + + constructor(private readonly bucket: s3.IBucket, { originAccessIdentity, ...props }: S3OriginProps) { + super(bucket.bucketRegionalDomainName, props); + + if (originAccessIdentity) { + this.originAccessIdentity = originAccessIdentity; + } + } + public bind(scope: Construct, options: cloudfront.OriginBindOptions): cloudfront.OriginBindConfig { - if (this.originAccessControl || FeatureFlags.of(scope).isEnabled(cxapi.CLOUDFRONT_USE_ORIGIN_ACCESS_CONTROL)) { - return this.bindOAC(scope, options); - } else if (!this.originAccessIdentity && !(FeatureFlags.of(scope).isEnabled(cxapi.CLOUDFRONT_USE_ORIGIN_ACCESS_CONTROL))) { + if (!this.originAccessIdentity) { // Using a bucket from another stack creates a cyclic reference with // the bucket taking a dependency on the generated S3CanonicalUserId for the grant principal, // and the distribution having a dependency on the bucket's domain name. @@ -215,9 +213,6 @@ class S3BucketOrigin extends cloudfront.OriginBase { } protected renderS3OriginConfig(): cloudfront.CfnDistribution.S3OriginConfigProperty | undefined { - if (this.originAccessIdentity) { - return { originAccessIdentity: `origin-access-identity/cloudfront/${this.originAccessIdentity.originAccessIdentityId}` }; - } - return { originAccessIdentity: '' }; + return { originAccessIdentity: `origin-access-identity/cloudfront/${this.originAccessIdentity.originAccessIdentityId}` }; } } diff --git a/packages/aws-cdk-lib/aws-cloudfront/lib/origin-access-control.ts b/packages/aws-cdk-lib/aws-cloudfront/lib/origin-access-control.ts index 21ef2b4905d08..8e8ff575aae33 100644 --- a/packages/aws-cdk-lib/aws-cloudfront/lib/origin-access-control.ts +++ b/packages/aws-cdk-lib/aws-cloudfront/lib/origin-access-control.ts @@ -123,49 +123,6 @@ export class OriginAccessControl extends OriginAccessControlBase { } return new Import(scope, id); } - /** - * docstring - */ - public static s3OriginAccessControl(scope: Construct, id: string, props?: OriginAccessControlProps): IOriginAccessControl { - return new OriginAccessControl(scope, id, { - description: props?.description, - originAccessControlName: props?.originAccessControlName, - originAccessControlOriginType: OriginAccessControlOriginType.S3, - }); - } - - /** - * docstring - */ - public static mediastoreOriginAccessControl(scope: Construct, id: string, props?: OriginAccessControlProps): IOriginAccessControl { - return new OriginAccessControl(scope, id, { - description: props?.description, - originAccessControlName: props?.originAccessControlName, - originAccessControlOriginType: OriginAccessControlOriginType.MEDIASTORE, - }); - } - - /** - * docstring - */ - public static lambdaOriginAccessControl(scope: Construct, id: string, props?: OriginAccessControlProps): IOriginAccessControl { - return new OriginAccessControl(scope, id, { - description: props?.description, - originAccessControlName: props?.originAccessControlName, - originAccessControlOriginType: OriginAccessControlOriginType.LAMBDA, - }); - } - - /** - * docstring - */ - public static mediapackageOriginAccessControl(scope: Construct, id: string, props?: OriginAccessControlProps): IOriginAccessControl { - return new OriginAccessControl(scope, id, { - description: props?.description, - originAccessControlName: props?.originAccessControlName, - originAccessControlOriginType: OriginAccessControlOriginType.MEDIAPACKAGEV2, - }); - } public readonly originAccessControlId: string; constructor(scope: Construct, id: string, props: OriginAccessControlProps = {}) { From 26b86c247298c10b1e5ef4324a39a673bac44af4 Mon Sep 17 00:00:00 2001 From: gracelu0 Date: Mon, 3 Jun 2024 09:47:41 -0700 Subject: [PATCH 06/14] fix undefined distribution id --- packages/aws-cdk-lib/aws-cloudfront-origins/lib/s3-origin.ts | 5 +++-- .../aws-cdk-lib/aws-cloudfront/lib/origin-access-control.ts | 4 +++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/aws-cdk-lib/aws-cloudfront-origins/lib/s3-origin.ts b/packages/aws-cdk-lib/aws-cloudfront-origins/lib/s3-origin.ts index 8fe8ffb58b0a5..0927d146cebda 100644 --- a/packages/aws-cdk-lib/aws-cloudfront-origins/lib/s3-origin.ts +++ b/packages/aws-cdk-lib/aws-cloudfront-origins/lib/s3-origin.ts @@ -3,7 +3,7 @@ import { HttpOrigin } from './http-origin'; import * as cloudfront from '../../aws-cloudfront'; import * as iam from '../../aws-iam'; import * as s3 from '../../aws-s3'; -import { Stack, Names, FeatureFlags, Aws, CustomResource, Annotations } from '../../core'; +import { Stack, Names, FeatureFlags, Aws, CustomResource, Annotations, Lazy } from '../../core'; import { S3OriginAccessControlBucketPolicyProvider } from '../../custom-resource-handlers/dist/aws-cloudfront-origins/s3-origin-access-control-bucket-policy-provider.generated'; import { S3OriginAccessControlKeyPolicyProvider } from '../../custom-resource-handlers/dist/aws-cloudfront-origins/s3-origin-access-control-key-policy-provider.generated'; import * as cxapi from '../../cx-api'; @@ -80,7 +80,8 @@ class S3BucketOacOrigin extends cloudfront.OriginBase { // Create a new origin access control if not specified this.originAccessControl = new cloudfront.OriginAccessControl(scope, 'S3OriginAccessControl'); } - const distributionId = options.distributionId; + const distribution = scope.node.scope as cloudfront.Distribution; + const distributionId = Lazy.string({ produce: () => distribution.distributionId }); const oacReadOnlyBucketPolicyStatement = new iam.PolicyStatement( { sid: 'AllowS3OACAccess', diff --git a/packages/aws-cdk-lib/aws-cloudfront/lib/origin-access-control.ts b/packages/aws-cdk-lib/aws-cloudfront/lib/origin-access-control.ts index 8e8ff575aae33..8fbb3fdd5156a 100644 --- a/packages/aws-cdk-lib/aws-cloudfront/lib/origin-access-control.ts +++ b/packages/aws-cdk-lib/aws-cloudfront/lib/origin-access-control.ts @@ -4,7 +4,6 @@ import { IResource, Resource, Stack, Names } from '../../core'; /** * Interface for CloudFront origin access controls */ -// extends iam.IGrantable?? export interface IOriginAccessControl extends IResource { /** * The unique identifier of the origin access control. @@ -14,6 +13,9 @@ export interface IOriginAccessControl extends IResource { } abstract class OriginAccessControlBase extends Resource implements IOriginAccessControl { + /** + * The unique identifier of the origin access control. + */ public abstract readonly originAccessControlId: string; } From a626a19dc272356f4eef7813a638d593d10b84e5 Mon Sep 17 00:00:00 2001 From: gracelu0 Date: Wed, 19 Jun 2024 15:14:39 -0700 Subject: [PATCH 07/14] refactor --- .../aws-cloudfront-origins/lib/s3-origin.ts | 312 ++++++++++-------- .../aws-cloudfront/lib/distribution.ts | 2 +- .../lib/origin-access-control.ts | 36 +- .../test/origin-access-control.test.ts | 0 packages/aws-cdk-lib/cx-api/FEATURE_FLAGS.md | 22 +- 5 files changed, 202 insertions(+), 170 deletions(-) create mode 100644 packages/aws-cdk-lib/aws-cloudfront/test/origin-access-control.test.ts diff --git a/packages/aws-cdk-lib/aws-cloudfront-origins/lib/s3-origin.ts b/packages/aws-cdk-lib/aws-cloudfront-origins/lib/s3-origin.ts index 0927d146cebda..87a87fe3e5bb6 100644 --- a/packages/aws-cdk-lib/aws-cloudfront-origins/lib/s3-origin.ts +++ b/packages/aws-cdk-lib/aws-cloudfront-origins/lib/s3-origin.ts @@ -2,8 +2,9 @@ import { Construct } from 'constructs'; import { HttpOrigin } from './http-origin'; import * as cloudfront from '../../aws-cloudfront'; import * as iam from '../../aws-iam'; +import { IKey } from '../../aws-kms'; import * as s3 from '../../aws-s3'; -import { Stack, Names, FeatureFlags, Aws, CustomResource, Annotations, Lazy } from '../../core'; +import { Stack, Names, FeatureFlags, Aws, CustomResource, Annotations } from '../../core'; import { S3OriginAccessControlBucketPolicyProvider } from '../../custom-resource-handlers/dist/aws-cloudfront-origins/s3-origin-access-control-bucket-policy-provider.generated'; import { S3OriginAccessControlKeyPolicyProvider } from '../../custom-resource-handlers/dist/aws-cloudfront-origins/s3-origin-access-control-key-policy-provider.generated'; import * as cxapi from '../../cx-api'; @@ -26,6 +27,13 @@ export interface S3OriginProps extends cloudfront.OriginProps { * @default - An Origin Access Control will be created. */ readonly originAccessControl?: cloudfront.IOriginAccessControl; + + /** + * When set to 'true', an attempt will be made to update the bucket policy to allow the + * CloudFront distribution access. + * @default false + */ + readonly overrideImportedBucketPolicy?: boolean; } /** @@ -49,9 +57,9 @@ export class S3Origin implements cloudfront.IOrigin { ...props, }); } else if (props.originAccessIdentity || !FeatureFlags.of(bucket.stack).isEnabled(cxapi.CLOUDFRONT_USE_ORIGIN_ACCESS_CONTROL)) { - this.origin = new S3BucketOrigin(bucket, props); + this.origin = S3BucketOrigin.withAccessIdentity(bucket, props); } else { - this.origin = new S3BucketOacOrigin(bucket, props); + this.origin = S3BucketOrigin.withAccessControl(bucket, props); } } @@ -63,157 +71,185 @@ export class S3Origin implements cloudfront.IOrigin { /** * An Origin specific to a S3 bucket (not configured for website hosting). * - * Contains additional logic around bucket permissions and origin access controls. + * Contains additional logic around bucket permissions and origin access control (via OAI or OAC). */ -class S3BucketOacOrigin extends cloudfront.OriginBase { - private originAccessControl!: cloudfront.IOriginAccessControl; - - constructor(private readonly bucket: s3.IBucket, { originAccessControl, ...props }: S3OriginProps) { - super(bucket.bucketRegionalDomainName, props); - if (originAccessControl) { - this.originAccessControl = originAccessControl; - } +abstract class S3BucketOrigin extends cloudfront.OriginBase { + public static withAccessIdentity(bucket: s3.IBucket, props: S3OriginProps = {}): S3BucketOrigin { + return new (class OriginAccessIdentity extends S3BucketOrigin { + private originAccessIdentity?: cloudfront.IOriginAccessIdentity; + + public constructor() { + super(bucket, props); + this.originAccessIdentity = props.originAccessIdentity; + } + + public bind(scope: Construct, options: cloudfront.OriginBindOptions): cloudfront.OriginBindConfig { + if (!this.originAccessIdentity) { + // Using a bucket from another stack creates a cyclic reference with + // the bucket taking a dependency on the generated S3CanonicalUserId for the grant principal, + // and the distribution having a dependency on the bucket's domain name. + // Fix this by parenting the OAI in the bucket's stack when cross-stack usage is detected. + const bucketStack = Stack.of(this.bucket); + const bucketInDifferentStack = bucketStack !== Stack.of(scope); + const oaiScope = bucketInDifferentStack ? bucketStack : scope; + const oaiId = bucketInDifferentStack ? `${Names.uniqueId(scope)}S3Origin` : 'S3Origin'; + + 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; currently, OAI only supports SSE-S3 for buckets. + // Source: https://aws.amazon.com/blogs/networking-and-content-delivery/serving-sse-kms-encrypted-content-from-s3-using-cloudfront/ + this.bucket.addToResourcePolicy(new iam.PolicyStatement({ + resources: [this.bucket.arnForObjects('*')], + actions: ['s3:GetObject'], + principals: [this.originAccessIdentity.grantPrincipal], + })); + return this._bind(scope, options); + } + + protected renderS3OriginConfig(): cloudfront.CfnDistribution.S3OriginConfigProperty | undefined { + if (!this.originAccessIdentity) { + throw new Error('Origin access identity cannot be undefined'); + } + return { originAccessIdentity: `origin-access-identity/cloudfront/${this.originAccessIdentity.originAccessIdentityId}` }; + } + })(); } - public bind(scope: Construct, options: cloudfront.OriginBindOptions): cloudfront.OriginBindConfig { - if (!this.originAccessControl) { - // Create a new origin access control if not specified - this.originAccessControl = new cloudfront.OriginAccessControl(scope, 'S3OriginAccessControl'); - } - const distribution = scope.node.scope as cloudfront.Distribution; - const distributionId = Lazy.string({ produce: () => distribution.distributionId }); - const oacReadOnlyBucketPolicyStatement = new iam.PolicyStatement( - { - sid: 'AllowS3OACAccess', - effect: iam.Effect.ALLOW, - principals: [new iam.ServicePrincipal('cloudfront.amazonaws.com')], - actions: ['s3:GetObject'], - resources: [this.bucket.arnForObjects('*')], - conditions: { - StringEquals: { - 'AWS:SourceArn': `arn:${Aws.PARTITION}:cloudfront::${Aws.ACCOUNT_ID}:distribution/${distributionId}`, + public static withAccessControl(bucket: s3.IBucket, props: S3OriginProps = {}): S3BucketOrigin { + return new (class OriginAccessControl extends S3BucketOrigin { + private originAccessControl?: cloudfront.IOriginAccessControl; + + constructor() { + super(bucket, props); + this.originAccessControl = props.originAccessControl; + } + + public bind(scope: Construct, options: cloudfront.OriginBindOptions): cloudfront.OriginBindConfig { + if (!this.originAccessControl) { + // Create a new origin access control if not specified + this.originAccessControl = new cloudfront.OriginAccessControl(scope, 'S3OriginAccessControl'); + } + const distributionId = options.distributionId; + const result = this.grantDistributionAccessToBucket(distributionId); + + // Failed to update bucket policy, assume using imported bucket + if (!result.statementAdded) { + if (props.overrideImportedBucketPolicy) { + this.grantDistributionAccessToImportedBucket(scope, distributionId); + } else { + Annotations.of(scope).addWarningV2('@aws-cdk/aws-cloudfront-origins:updateBucketPolicy', + 'Cannot update bucket policy of an imported bucket. Set overrideImportedBucketPolicy to true or update the policy manually instead.'); + } + } + + if (this.bucket.encryptionKey) { + this.grantDistributionAccessToKey(scope, distributionId, this.bucket.encryptionKey); + } + + const originBindConfig = this._bind(scope, options); + + // Update configuration to set OriginControlAccessId property + return { + ...originBindConfig, + originProperty: { + ...originBindConfig.originProperty!, + originAccessControlId: this.originAccessControl.originAccessControlId, }, - }, - }, - ); - const result = this.bucket.addToResourcePolicy(oacReadOnlyBucketPolicyStatement); - - // Failed to update bucket policy, assume using imported bucket - if (!result.statementAdded) { - Annotations.of(scope).addWarningV2('@aws-cdk/aws-cloudfront-origins:updateBucketPolicy', 'Cannot update bucket policy of an imported bucket. Update the policy manually instead.'); - const provider = S3OriginAccessControlBucketPolicyProvider.getOrCreateProvider(scope, S3_ORIGIN_ACCESS_CONTROL_BUCKET_RESOURCE_TYPE, - { + }; + } + + /** + * If you're using origin access control (OAC) instead of origin access identity, specify an empty `OriginAccessIdentity` element. + * @see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cloudfront-distribution-s3originconfig.html#cfn-cloudfront-distribution-s3originconfig-originaccessidentity + */ + protected renderS3OriginConfig(): cloudfront.CfnDistribution.S3OriginConfigProperty | undefined { + return { originAccessIdentity: '' }; + } + + private grantDistributionAccessToBucket(distributionId: string): iam.AddToResourcePolicyResult { + const oacReadOnlyBucketPolicyStatement = new iam.PolicyStatement( + { + effect: iam.Effect.ALLOW, + principals: [new iam.ServicePrincipal('cloudfront.amazonaws.com')], + actions: ['s3:GetObject'], + resources: [this.bucket.arnForObjects('*')], + conditions: { + StringEquals: { + 'AWS:SourceArn': `arn:${Aws.PARTITION}:cloudfront::${Aws.ACCOUNT_ID}:distribution/${distributionId}`, + }, + }, + }, + ); + const result = this.bucket.addToResourcePolicy(oacReadOnlyBucketPolicyStatement); + return result; + } + + /** + * Use custom resource to update bucket policy and remove OAI policy statement if it exists + */ + private grantDistributionAccessToImportedBucket(scope: Construct, distributionId: string) { + const provider = S3OriginAccessControlBucketPolicyProvider.getOrCreateProvider(scope, S3_ORIGIN_ACCESS_CONTROL_BUCKET_RESOURCE_TYPE, { description: 'Lambda function that updates S3 bucket policy to allow CloudFront distribution access.', }); - provider.addToRolePolicy({ - Action: ['s3:getBucketPolicy', 's3:putBucketPolicy'], - Effect: 'Allow', - Resource: [this.bucket.bucketArn], - }); - - new CustomResource(scope, 'S3OriginBucketPolicyCustomResource', { - resourceType: S3_ORIGIN_ACCESS_CONTROL_BUCKET_RESOURCE_TYPE, - serviceToken: provider.serviceToken, - properties: { - DistributionId: distributionId, - AccountId: this.bucket.env.account, - Partition: Stack.of(scope).partition, - BucketName: this.bucket.bucketName, - IsImportedBucket: !result.statementAdded, - }, - }); - } - - if (this.bucket.encryptionKey) { - const provider = S3OriginAccessControlKeyPolicyProvider.getOrCreateProvider(scope, S3_ORIGIN_ACCESS_CONTROL_KEY_RESOURCE_TYPE, - { - description: 'Lambda function that updates SSE-KMS key policy to allow CloudFront distribution access.', + provider.addToRolePolicy({ + Action: ['s3:getBucketPolicy', 's3:putBucketPolicy'], + Effect: 'Allow', + Resource: [this.bucket.bucketArn], }); - provider.addToRolePolicy({ - Action: ['kms:PutKeyPolicy', 'kms:GetKeyPolicy', 'kms:DescribeKey'], - Effect: 'Allow', - Resource: [this.bucket.encryptionKey.keyArn], - }); - new CustomResource(scope, 'S3OriginKMSKeyPolicyCustomResource', { - resourceType: S3_ORIGIN_ACCESS_CONTROL_KEY_RESOURCE_TYPE, - serviceToken: provider.serviceToken, - properties: { - DistributionId: distributionId, - KmsKeyId: this.bucket.encryptionKey.keyId, - AccountId: this.bucket.env.account, - Partition: Stack.of(scope).partition, - }, - }); - } - - const originBindConfig = super.bind(scope, options); - - // Update configuration to set OriginControlAccessId property - return { - ...originBindConfig, - originProperty: { - ...originBindConfig.originProperty!, - originAccessControlId: this.originAccessControl.originAccessControlId, - }, - }; - } + new CustomResource(scope, 'S3OriginBucketPolicyCustomResource', { + resourceType: S3_ORIGIN_ACCESS_CONTROL_BUCKET_RESOURCE_TYPE, + serviceToken: provider.serviceToken, + properties: { + DistributionId: distributionId, + AccountId: this.bucket.env.account, + Partition: Stack.of(scope).partition, + BucketName: this.bucket.bucketName, + }, + }); + } + + /** + * Use custom resource to update KMS key policy + */ + private grantDistributionAccessToKey(scope: Construct, distributionId: string, key: IKey) { + const provider = S3OriginAccessControlKeyPolicyProvider.getOrCreateProvider(scope, S3_ORIGIN_ACCESS_CONTROL_KEY_RESOURCE_TYPE, + { + description: 'Lambda function that updates SSE-KMS key policy to allow CloudFront distribution access.', + }); + provider.addToRolePolicy({ + Action: ['kms:PutKeyPolicy', 'kms:GetKeyPolicy', 'kms:DescribeKey'], + Effect: 'Allow', + Resource: [key.keyArn], + }); - /** - * If you're using origin access control (OAC) instead of origin access identity, specify an empty `OriginAccessIdentity` element. - * @see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-cloudfront-distribution-s3originconfig.html#cfn-cloudfront-distribution-s3originconfig-originaccessidentity - */ - protected renderS3OriginConfig(): cloudfront.CfnDistribution.S3OriginConfigProperty | undefined { - return { originAccessIdentity: '' }; + new CustomResource(scope, 'S3OriginKMSKeyPolicyCustomResource', { + resourceType: S3_ORIGIN_ACCESS_CONTROL_KEY_RESOURCE_TYPE, + serviceToken: provider.serviceToken, + properties: { + DistributionId: distributionId, + KmsKeyId: key.keyId, + AccountId: this.bucket.env.account, + Partition: Stack.of(scope).partition, + }, + }); + } + }); } -} -/** - * An Origin specific to a S3 bucket (not configured for website hosting). - * - * Contains additional logic around bucket permissions and origin access identities. - */ -class S3BucketOrigin extends cloudfront.OriginBase { - private originAccessIdentity!: cloudfront.IOriginAccessIdentity; - - constructor(private readonly bucket: s3.IBucket, { originAccessIdentity, ...props }: S3OriginProps) { + protected constructor(protected readonly bucket: s3.IBucket, props: S3OriginProps = {}) { super(bucket.bucketRegionalDomainName, props); - - if (originAccessIdentity) { - this.originAccessIdentity = originAccessIdentity; - } } - public bind(scope: Construct, options: cloudfront.OriginBindOptions): cloudfront.OriginBindConfig { - if (!this.originAccessIdentity) { - // Using a bucket from another stack creates a cyclic reference with - // the bucket taking a dependency on the generated S3CanonicalUserId for the grant principal, - // and the distribution having a dependency on the bucket's domain name. - // Fix this by parenting the OAI in the bucket's stack when cross-stack usage is detected. - const bucketStack = Stack.of(this.bucket); - const bucketInDifferentStack = bucketStack !== Stack.of(scope); - const oaiScope = bucketInDifferentStack ? bucketStack : scope; - const oaiId = bucketInDifferentStack ? `${Names.uniqueId(scope)}S3Origin` : 'S3Origin'; - - this.originAccessIdentity = new cloudfront.OriginAccessIdentity(oaiScope, oaiId, { - comment: `Identity for ${options.originId}`, - }); + public abstract bind(scope: Construct, options: cloudfront.OriginBindOptions): cloudfront.OriginBindConfig; - // 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; currently, OAI only supports SSE-S3 for buckets. - // Source: https://aws.amazon.com/blogs/networking-and-content-delivery/serving-sse-kms-encrypted-content-from-s3-using-cloudfront/ - this.bucket.addToResourcePolicy(new iam.PolicyStatement({ - resources: [this.bucket.arnForObjects('*')], - actions: ['s3:GetObject'], - principals: [this.originAccessIdentity.grantPrincipal], - })); - }; + protected abstract renderS3OriginConfig(): cloudfront.CfnDistribution.S3OriginConfigProperty | undefined; + protected _bind(scope: Construct, options: cloudfront.OriginBindOptions): cloudfront.OriginBindConfig { return super.bind(scope, options); } - - protected renderS3OriginConfig(): cloudfront.CfnDistribution.S3OriginConfigProperty | undefined { - return { originAccessIdentity: `origin-access-identity/cloudfront/${this.originAccessIdentity.originAccessIdentityId}` }; - } } diff --git a/packages/aws-cdk-lib/aws-cloudfront/lib/distribution.ts b/packages/aws-cdk-lib/aws-cloudfront/lib/distribution.ts index cbda930c0fc83..e287cb7d60c15 100644 --- a/packages/aws-cdk-lib/aws-cloudfront/lib/distribution.ts +++ b/packages/aws-cdk-lib/aws-cloudfront/lib/distribution.ts @@ -609,7 +609,7 @@ export class Distribution extends Resource implements IDistribution { const scope = new Construct(this, `Origin${originIndex}`); const generatedId = Names.uniqueId(scope).slice(-ORIGIN_ID_MAX_LENGTH); const distributionId = this.distributionId; - const originBindConfig = origin.bind(scope, { originId: generatedId, distributionId }); + const originBindConfig = origin.bind(scope, { originId: generatedId, distributionId: Lazy.string({ produce: () => this.distributionId }) }); const originId = originBindConfig.originProperty?.id ?? generatedId; const duplicateId = this.boundOrigins.find(boundOrigin => boundOrigin.originProperty?.id === originBindConfig.originProperty?.id); if (duplicateId) { diff --git a/packages/aws-cdk-lib/aws-cloudfront/lib/origin-access-control.ts b/packages/aws-cdk-lib/aws-cloudfront/lib/origin-access-control.ts index 8fbb3fdd5156a..20834abf09e24 100644 --- a/packages/aws-cdk-lib/aws-cloudfront/lib/origin-access-control.ts +++ b/packages/aws-cdk-lib/aws-cloudfront/lib/origin-access-control.ts @@ -1,6 +1,6 @@ import { Construct } from 'constructs'; import { CfnOriginAccessControl } from './cloudfront.generated'; -import { IResource, Resource, Stack, Names } from '../../core'; +import { IResource, Resource, Names } from '../../core'; /** * Interface for CloudFront origin access controls */ @@ -12,13 +12,6 @@ export interface IOriginAccessControl extends IResource { readonly originAccessControlId: string; } -abstract class OriginAccessControlBase extends Resource implements IOriginAccessControl { - /** - * The unique identifier of the origin access control. - */ - public abstract readonly originAccessControlId: string; -} - /** * Properties for creating a OriginAccessControl resource. */ @@ -35,17 +28,17 @@ export interface OriginAccessControlProps { readonly originAccessControlName?: string; /** * The type of origin that this origin access control is for. - * @default s3 + * @default OriginAccessControlOriginType.S3 */ readonly originAccessControlOriginType?: OriginAccessControlOriginType; /** * Specifies which requests CloudFront signs. - * @default always + * @default SigningBehavior.ALWAYS */ readonly signingBehavior?: SigningBehavior; /** * The signing protocol of the origin access control. - * @default sigv4 + * @default SigningProtocol.SIGV4 */ readonly signingProtocol?: SigningProtocol; } @@ -110,12 +103,12 @@ export enum SigningProtocol { * @resource AWS::CloudFront::OriginAccessControl * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-cloudfront-originaccesscontrol.html */ -export class OriginAccessControl extends OriginAccessControlBase { +export class OriginAccessControl extends Resource implements IOriginAccessControl { /** * Imports an origin access control from its id. */ public static fromOriginAccessControlId(scope: Construct, id: string, originAccessControlId: string): IOriginAccessControl { - class Import extends OriginAccessControlBase { + class Import extends Resource implements IOriginAccessControl { public readonly originAccessControlId = originAccessControlId; constructor(s: Construct, i: string) { super(s, i); @@ -126,14 +119,20 @@ export class OriginAccessControl extends OriginAccessControlBase { return new Import(scope, id); } + /** + * Returns the Id of this OriginAccessControl + */ public readonly originAccessControlId: string; + constructor(scope: Construct, id: string, props: OriginAccessControlProps = {}) { super(scope, id); const resource = new CfnOriginAccessControl(this, 'Resource', { originAccessControlConfig: { description: props.description, - name: props.originAccessControlName ?? this.generateName(), + name: props.originAccessControlName ?? Names.uniqueResourceName(this, { + maxLength: 64, + }), signingBehavior: props.signingBehavior ?? SigningBehavior.ALWAYS, signingProtocol: props.signingProtocol ?? SigningProtocol.SIGV4, originAccessControlOriginType: props.originAccessControlOriginType ?? OriginAccessControlOriginType.S3, @@ -142,13 +141,4 @@ export class OriginAccessControl extends OriginAccessControlBase { this.originAccessControlId = resource.attrId; } - - private generateName(): string { - const name = Stack.of(this).region + Names.uniqueId(this); - if (name.length > 64) { - return name.substring(0, 32) + name.substring(name.length - 32); - } - return name; - } - } \ No newline at end of file diff --git a/packages/aws-cdk-lib/aws-cloudfront/test/origin-access-control.test.ts b/packages/aws-cdk-lib/aws-cloudfront/test/origin-access-control.test.ts new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/packages/aws-cdk-lib/cx-api/FEATURE_FLAGS.md b/packages/aws-cdk-lib/cx-api/FEATURE_FLAGS.md index 59e79334b7042..2f4e05544d91a 100644 --- a/packages/aws-cdk-lib/cx-api/FEATURE_FLAGS.md +++ b/packages/aws-cdk-lib/cx-api/FEATURE_FLAGS.md @@ -70,15 +70,18 @@ Flags come in three types: | [@aws-cdk/aws-eks:nodegroupNameAttribute](#aws-cdkaws-eksnodegroupnameattribute) | When enabled, nodegroupName attribute of the provisioned EKS NodeGroup will not have the cluster name prefix. | 2.139.0 | (fix) | | [@aws-cdk/aws-ec2:ebsDefaultGp3Volume](#aws-cdkaws-ec2ebsdefaultgp3volume) | When enabled, the default volume type of the EBS volume will be GP3 | 2.140.0 | (default) | | [@aws-cdk/pipelines:reduceAssetRoleTrustScope](#aws-cdkpipelinesreduceassetroletrustscope) | Remove the root account principal from PipelineAssetsFileRole trust policy | 2.141.0 | (default) | -<<<<<<< HEAD | [@aws-cdk/aws-ecs:removeDefaultDeploymentAlarm](#aws-cdkaws-ecsremovedefaultdeploymentalarm) | When enabled, remove default deployment alarm settings | 2.143.0 | (default) | <<<<<<< HEAD +<<<<<<< HEAD | [@aws-cdk/custom-resources:logApiResponseDataPropertyTrueDefault](#aws-cdkcustom-resourceslogapiresponsedatapropertytruedefault) | When enabled, the custom resource used for `AwsCustomResource` will configure the `logApiResponseData` property as true by default | 2.145.0 | (fix) | ======= ======= | [@aws-cdk/aws-cloudfront:useOriginAccessControl](#aws-cdkaws-cloudfrontuseoriginaccesscontrol) | When enabled, use Origin Access Control rather than Origin Access Identity | V2NEXT | (fix) | >>>>>>> 386904900a (wip oac) >>>>>>> c47258bd5d (wip oac) +======= +| [@aws-cdk/aws-cloudfront:useOriginAccessControl](#aws-cdkaws-cloudfrontuseoriginaccesscontrol) | When enabled, an origin access control will be created automatically when a new S3 origin is created. | V2NEXT | (fix) | +>>>>>>> a76e6cc968 (refactor) @@ -139,6 +142,7 @@ The following json shows the current recommended set of flags, as `cdk init` wou "@aws-cdk/aws-kms:reduceCrossAccountRegionPolicyScope": true, "@aws-cdk/aws-eks:nodegroupNameAttribute": true, "@aws-cdk/aws-ec2:ebsDefaultGp3Volume": true, +<<<<<<< HEAD <<<<<<< HEAD "@aws-cdk/aws-ecs:removeDefaultDeploymentAlarm": true, "@aws-cdk/custom-resources:logApiResponseDataPropertyTrueDefault": false @@ -149,6 +153,10 @@ The following json shows the current recommended set of flags, as `cdk init` wou "@aws-cdk/aws-cloudfront:useOriginAccessControl": true, >>>>>>> 386904900a (wip oac) >>>>>>> c47258bd5d (wip oac) +======= + "@aws-cdk/aws-ecs:removeDefaultDeploymentAlarm": true, + "@aws-cdk/aws-cloudfront:useOriginAccessControl": true +>>>>>>> a76e6cc968 (refactor) } } ``` @@ -1309,7 +1317,7 @@ any prefix. *When enabled, the default volume type of the EBS volume will be GP3* (default) -When this featuer flag is enabled, the default volume type of the EBS volume will be `EbsDeviceVolumeType.GENERAL_PURPOSE_SSD_GP3`. +When this feature flag is enabled, the default volume type of the EBS volume will be `EbsDeviceVolumeType.GENERAL_PURPOSE_SSD_GP3`. | Since | Default | Recommended | @@ -1340,7 +1348,7 @@ When this feature flag is disabled, it will keep the root account principal in t *When enabled, remove default deployment alarm settings* (default) -When this feature flag is enabled, remove the default deployment alarm settings when creating a AWS ECS service. +When this featuer flag is enabled, remove the default deployment alarm settings when creating a AWS ECS service. | Since | Default | Recommended | @@ -1372,17 +1380,15 @@ property from the event object. ### @aws-cdk/aws-cloudfront:useOriginAccessControl -*Use Origin Access Control instead of Origin Access Identity* (fix) - -When this feature flag is enabled, an origin access control will be created automatically when a new S3 origin is created. -When this feature flag is disabled, an origin access identity will be created automatically when a new S3 origin is created. +*When enabled, an origin access control will be created automatically when a new S3 origin is created.* (fix) +When this feature flag is enabled, an origin access control will be created automatically when a new `S3Origin` is created instead +of an origin access identity (legacy). | Since | Default | Recommended | | ----- | ----- | ----- | | (not in v1) | | | | V2NEXT | `false` | `true` | -**Compatibility with old behavior:** Disable the feature flag to continue using Origin Access Identity From 0ecd6dc8cabd7c03eceecf5fbd3efaa00b3464d5 Mon Sep 17 00:00:00 2001 From: gracelu0 Date: Thu, 20 Jun 2024 10:51:27 -0700 Subject: [PATCH 08/14] Add validation for origin type on OAC --- .../aws-cloudfront-origins/lib/s3-origin.ts | 7 ++- .../lib/origin-access-control.ts | 60 ++++++++++++++++++- .../aws-cloudfront/lib/web-distribution.ts | 1 + 3 files changed, 65 insertions(+), 3 deletions(-) diff --git a/packages/aws-cdk-lib/aws-cloudfront-origins/lib/s3-origin.ts b/packages/aws-cdk-lib/aws-cloudfront-origins/lib/s3-origin.ts index 87a87fe3e5bb6..7994e59361bfb 100644 --- a/packages/aws-cdk-lib/aws-cloudfront-origins/lib/s3-origin.ts +++ b/packages/aws-cdk-lib/aws-cloudfront-origins/lib/s3-origin.ts @@ -131,8 +131,13 @@ abstract class S3BucketOrigin extends cloudfront.OriginBase { public bind(scope: Construct, options: cloudfront.OriginBindOptions): cloudfront.OriginBindConfig { if (!this.originAccessControl) { // Create a new origin access control if not specified - this.originAccessControl = new cloudfront.OriginAccessControl(scope, 'S3OriginAccessControl'); + this.originAccessControl = new cloudfront.S3OriginAccessControl(scope, 'S3OriginAccessControl'); } + + if (!cloudfront.S3OriginAccessControl.isS3OriginAccessControl(this.originAccessControl)) { + throw new Error('Origin access control for an S3 origin must be a S3OriginAccessControl'); + } + const distributionId = options.distributionId; const result = this.grantDistributionAccessToBucket(distributionId); diff --git a/packages/aws-cdk-lib/aws-cloudfront/lib/origin-access-control.ts b/packages/aws-cdk-lib/aws-cloudfront/lib/origin-access-control.ts index 20834abf09e24..f510675896182 100644 --- a/packages/aws-cdk-lib/aws-cloudfront/lib/origin-access-control.ts +++ b/packages/aws-cdk-lib/aws-cloudfront/lib/origin-access-control.ts @@ -1,6 +1,10 @@ import { Construct } from 'constructs'; import { CfnOriginAccessControl } from './cloudfront.generated'; import { IResource, Resource, Names } from '../../core'; + +const S3_ORIGIN_ACCESS_CONTROL_SYMBOL = Symbol.for('aws-cdk-lib/aws-cloudfront/lib/origin-access-control.S3OriginAccessControl'); +const LAMBDA_ORIGIN_ACCESS_CONTROL_SYMBOL = Symbol.for('aws-cdk-lib/aws-cloudfront/lib/origin-access-control.LambdaOriginAccessControl'); + /** * Interface for CloudFront origin access controls */ @@ -119,8 +123,17 @@ export class OriginAccessControl extends Resource implements IOriginAccessContro return new Import(scope, id); } + public static forS3(scope: Construct, id: string, props: OriginAccessControlProps = {}) { + return new S3OriginAccessControl(scope, id, props); + } + + public static forLambda(scope: Construct, id: string, props: OriginAccessControlProps = {}) { + return new LambdaOriginAccessControl(scope, id, props); + } + /** - * Returns the Id of this OriginAccessControl + * The unique identifier of this Origin Access Control. + * @attribute */ public readonly originAccessControlId: string; @@ -141,4 +154,47 @@ export class OriginAccessControl extends Resource implements IOriginAccessContro this.originAccessControlId = resource.attrId; } -} \ No newline at end of file +} + +/** + * Origin access control for a S3 bucket origin + */ +export class S3OriginAccessControl extends OriginAccessControl { + /** + * Returns `true` if `x` is an S3OriginAccessControl, `false` otherwise + */ + public static isS3OriginAccessControl(x: any): x is S3OriginAccessControl { + return x !== null && typeof (x) === 'object' && S3_ORIGIN_ACCESS_CONTROL_SYMBOL in x; + } + constructor(scope: Construct, id: string, props: OriginAccessControlProps = {}) { + super(scope, id, { ...props, originAccessControlOriginType: OriginAccessControlOriginType.S3 }); + } +} + +Object.defineProperty(S3OriginAccessControl.prototype, S3_ORIGIN_ACCESS_CONTROL_SYMBOL, { + value: true, + enumerable: false, + writable: false, +}); + +/** + * Origin access control for a Lambda Function Url origin + */ +export class LambdaOriginAccessControl extends OriginAccessControl { + /** + * Returns `true` if `x` is a LambdaOriginAccessControl, `false` otherwise + */ + public static isLambdaOriginAccessControl(x: any): x is LambdaOriginAccessControl { + return x !== null && typeof (x) === 'object' && LAMBDA_ORIGIN_ACCESS_CONTROL_SYMBOL in x; + } + + constructor(scope: Construct, id: string, props: OriginAccessControlProps = {}) { + super(scope, id, { ...props, originAccessControlOriginType: OriginAccessControlOriginType.LAMBDA }); + } +} + +Object.defineProperty(LambdaOriginAccessControl.prototype, LAMBDA_ORIGIN_ACCESS_CONTROL_SYMBOL, { + value: true, + enumerable: false, + writable: false, +}); \ No newline at end of file diff --git a/packages/aws-cdk-lib/aws-cloudfront/lib/web-distribution.ts b/packages/aws-cdk-lib/aws-cloudfront/lib/web-distribution.ts index ba0fa61da0bba..fe60b6163a867 100644 --- a/packages/aws-cdk-lib/aws-cloudfront/lib/web-distribution.ts +++ b/packages/aws-cdk-lib/aws-cloudfront/lib/web-distribution.ts @@ -756,6 +756,7 @@ export interface CloudFrontWebDistributionAttributes { * You can customize the distribution using additional properties from the CloudFrontWebDistributionProps interface. * * @resource AWS::CloudFront::Distribution + * @deprecated Use `Distribution` instead */ export class CloudFrontWebDistribution extends cdk.Resource implements IDistribution { From 195307f40a8dedfc1263630555cba905e541d3a9 Mon Sep 17 00:00:00 2001 From: gracelu0 Date: Fri, 21 Jun 2024 10:45:52 -0700 Subject: [PATCH 09/14] Add origin type to oac --- .../test/integ.s3-origin-oac.ts | 0 .../index.ts | 30 +++-- .../index.ts | 30 +++-- .../aws-cloudfront-origins/lib/s3-origin.ts | 7 +- packages/aws-cdk-lib/aws-cloudfront/README.md | 4 +- .../lib/origin-access-control.ts | 117 ++++++------------ 6 files changed, 82 insertions(+), 106 deletions(-) create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront-origins/test/integ.s3-origin-oac.ts diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront-origins/test/integ.s3-origin-oac.ts b/packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront-origins/test/integ.s3-origin-oac.ts new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/packages/@aws-cdk/custom-resource-handlers/lib/aws-cloudfront-origins/s3-origin-access-control-bucket-policy-handler/index.ts b/packages/@aws-cdk/custom-resource-handlers/lib/aws-cloudfront-origins/s3-origin-access-control-bucket-policy-handler/index.ts index 3f7f357366c76..77117b1c546cd 100644 --- a/packages/@aws-cdk/custom-resource-handlers/lib/aws-cloudfront-origins/s3-origin-access-control-bucket-policy-handler/index.ts +++ b/packages/@aws-cdk/custom-resource-handlers/lib/aws-cloudfront-origins/s3-origin-access-control-bucket-policy-handler/index.ts @@ -21,11 +21,8 @@ export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent const accountId = props.AccountId; const partition = props.Partition; const bucketName = props.BucketName; - const isImportedBucket = props.IsImportedBucket; - if (isImportedBucket) { - await updateBucketPolicy({ bucketName, distributionId, partition, accountId }); - } + await updateBucketPolicy({ bucketName, distributionId, partition, accountId }); return { IsComplete: true, @@ -93,20 +90,29 @@ async function updateBucketPolicy(props: updateBucketPolicyProps) { * @returns currentPolicy - the updated policy. */ function updatePolicy(currentPolicy: any, policyStatementToAdd: any) { - // Check to see if a duplicate key policy exists by matching on the sid. This is to prevent duplicate key policies - // from being added/updated in response to a stack being updated one or more times after initial creation. - const existingPolicyIndex = currentPolicy.Statement.findIndex((statement: any) => statement.Sid === policyStatementToAdd.Sid); - // If a match is found, overwrite the key policy statement... - // Otherwise, push the new key policy to the array of statements - if (existingPolicyIndex > -1) { - currentPolicy.Statement[existingPolicyIndex] = policyStatementToAdd; - } else { + // // Check to see if a duplicate key policy exists by matching on the sid. This is to prevent duplicate key policies + // // from being added/updated in response to a stack being updated one or more times after initial creation. + // const existingPolicyIndex = currentPolicy.Statement.findIndex((statement: any) => statement.Sid === policyStatementToAdd.Sid); + // // If a match is found, overwrite the key policy statement... + // // Otherwise, push the new key policy to the array of statements + // if (existingPolicyIndex > -1) { + // currentPolicy.Statement[existingPolicyIndex] = policyStatementToAdd; + // } else { + // currentPolicy.Statement.push(policyStatementToAdd); + // } + + if (!isStatementInPolicy(currentPolicy, policyStatementToAdd)) { currentPolicy.Statement.push(policyStatementToAdd); } + // Return the result return currentPolicy; }; +function isStatementInPolicy(policy: any, statement: any): boolean { + return policy.Statement.some((existingStatement: any) => JSON.stringify(existingStatement) === JSON.stringify(statement)); +} + /** * Check if the policy contains an OAI principal */ diff --git a/packages/@aws-cdk/custom-resource-handlers/lib/aws-cloudfront-origins/s3-origin-access-control-key-policy-handler/index.ts b/packages/@aws-cdk/custom-resource-handlers/lib/aws-cloudfront-origins/s3-origin-access-control-key-policy-handler/index.ts index a77da0bc38c60..8dfb15b79adcd 100644 --- a/packages/@aws-cdk/custom-resource-handlers/lib/aws-cloudfront-origins/s3-origin-access-control-key-policy-handler/index.ts +++ b/packages/@aws-cdk/custom-resource-handlers/lib/aws-cloudfront-origins/s3-origin-access-control-key-policy-handler/index.ts @@ -37,6 +37,7 @@ export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent // Define the updated key policy to allow CloudFront Distribution access const keyPolicy = JSON.parse(getKeyPolicyCommandResponse?.Policy); + console.log('Retrieved key policy', JSON.stringify(keyPolicy, undefined, 2)); const kmsKeyPolicyStatement = { Sid: 'AllowCloudFrontServicePrincipalSSE-KMS', Effect: 'Allow', @@ -58,6 +59,7 @@ export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent }, }; const updatedKeyPolicy = updatePolicy(keyPolicy, kmsKeyPolicyStatement); + console.log('Updated key policy', JSON.stringify(updatedKeyPolicy, undefined, 2)); await kms.putKeyPolicy({ KeyId: kmsKeyId, Policy: JSON.stringify(updatedKeyPolicy), @@ -80,17 +82,25 @@ export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent * @param policyStatementToAdd - the policy statement to be added to the policy. * @returns currentPolicy - the updated policy. */ -export const updatePolicy = (currentPolicy: any, policyStatementToAdd: any) => { - // Check to see if a duplicate key policy exists by matching on the sid. This is to prevent duplicate key policies - // from being added/updated in response to a stack being updated one or more times after initial creation. - const existingPolicyIndex = currentPolicy.Statement.findIndex((statement: any) => statement.Sid === policyStatementToAdd.Sid); - // If a match is found, overwrite the key policy statement... - // Otherwise, push the new key policy to the array of statements - if (existingPolicyIndex > -1) { - currentPolicy.Statement[existingPolicyIndex] = policyStatementToAdd; - } else { +function updatePolicy(currentPolicy: any, policyStatementToAdd: any) { + // // Check to see if a duplicate key policy exists by matching on the sid. This is to prevent duplicate key policies + // // from being added/updated in response to a stack being updated one or more times after initial creation. + // const existingPolicyIndex = currentPolicy.Statement.findIndex((statement: any) => statement.Sid === policyStatementToAdd.Sid); + // // If a match is found, overwrite the key policy statement... + // // Otherwise, push the new key policy to the array of statements + // if (existingPolicyIndex > -1) { + // currentPolicy.Statement[existingPolicyIndex] = policyStatementToAdd; + // } else { + // currentPolicy.Statement.push(policyStatementToAdd); + // } + + if (!isStatementInPolicy(currentPolicy, policyStatementToAdd)) { currentPolicy.Statement.push(policyStatementToAdd); } // Return the result return currentPolicy; -}; \ No newline at end of file +}; + +function isStatementInPolicy(policy: any, statement: any): boolean { + return policy.Statement.some((existingStatement: any) => JSON.stringify(existingStatement) === JSON.stringify(statement)); +} \ No newline at end of file diff --git a/packages/aws-cdk-lib/aws-cloudfront-origins/lib/s3-origin.ts b/packages/aws-cdk-lib/aws-cloudfront-origins/lib/s3-origin.ts index 7994e59361bfb..d4e00c5ad1579 100644 --- a/packages/aws-cdk-lib/aws-cloudfront-origins/lib/s3-origin.ts +++ b/packages/aws-cdk-lib/aws-cloudfront-origins/lib/s3-origin.ts @@ -131,11 +131,11 @@ abstract class S3BucketOrigin extends cloudfront.OriginBase { public bind(scope: Construct, options: cloudfront.OriginBindOptions): cloudfront.OriginBindConfig { if (!this.originAccessControl) { // Create a new origin access control if not specified - this.originAccessControl = new cloudfront.S3OriginAccessControl(scope, 'S3OriginAccessControl'); + this.originAccessControl = new cloudfront.OriginAccessControl(scope, 'S3OriginAccessControl'); } - if (!cloudfront.S3OriginAccessControl.isS3OriginAccessControl(this.originAccessControl)) { - throw new Error('Origin access control for an S3 origin must be a S3OriginAccessControl'); + if (this.originAccessControl.originAccessControlOriginType !== cloudfront.OriginAccessControlOriginType.S3) { + throw new Error(`Origin access control for an S3 origin must have origin type '${cloudfront.OriginAccessControlOriginType.S3}', got origin type '${this.originAccessControl.originAccessControlOriginType}'`); } const distributionId = options.distributionId; @@ -178,6 +178,7 @@ abstract class S3BucketOrigin extends cloudfront.OriginBase { private grantDistributionAccessToBucket(distributionId: string): iam.AddToResourcePolicyResult { const oacReadOnlyBucketPolicyStatement = new iam.PolicyStatement( { + sid: 'AllowS3OACAccess', effect: iam.Effect.ALLOW, principals: [new iam.ServicePrincipal('cloudfront.amazonaws.com')], actions: ['s3:GetObject'], diff --git a/packages/aws-cdk-lib/aws-cloudfront/README.md b/packages/aws-cdk-lib/aws-cloudfront/README.md index 965e3cbac7f69..05d5dfff872ac 100644 --- a/packages/aws-cdk-lib/aws-cloudfront/README.md +++ b/packages/aws-cdk-lib/aws-cloudfront/README.md @@ -8,7 +8,7 @@ possible performance. ## Distribution API -The `Distribution` API is currently being built to replace the existing `CloudFrontWebDistribution` API. The `Distribution` API is optimized for the +The `Distribution` API replaces the `CloudFrontWebDistribution` API which is now deprecated. The `Distribution` API is optimized for the most common use cases of CloudFront distributions (e.g., single origin and behavior, few customizations) while still providing the ability for more advanced use cases. The API focuses on simplicity for the common use cases, and convenience methods for creating the behaviors and origins necessary for more complex use cases. @@ -951,7 +951,7 @@ If no changes are desired during migration, you will at the least be able to use ## CloudFrontWebDistribution API -> The `CloudFrontWebDistribution` construct is the original construct written for working with CloudFront distributions. +> The `CloudFrontWebDistribution` construct is the original construct written for working with CloudFront distributions and has been marked as deprecated. > Users are encouraged to use the newer `Distribution` instead, as it has a simpler interface and receives new features faster. Example usage: diff --git a/packages/aws-cdk-lib/aws-cloudfront/lib/origin-access-control.ts b/packages/aws-cdk-lib/aws-cloudfront/lib/origin-access-control.ts index f510675896182..203c376b90e8f 100644 --- a/packages/aws-cdk-lib/aws-cloudfront/lib/origin-access-control.ts +++ b/packages/aws-cdk-lib/aws-cloudfront/lib/origin-access-control.ts @@ -2,11 +2,8 @@ import { Construct } from 'constructs'; import { CfnOriginAccessControl } from './cloudfront.generated'; import { IResource, Resource, Names } from '../../core'; -const S3_ORIGIN_ACCESS_CONTROL_SYMBOL = Symbol.for('aws-cdk-lib/aws-cloudfront/lib/origin-access-control.S3OriginAccessControl'); -const LAMBDA_ORIGIN_ACCESS_CONTROL_SYMBOL = Symbol.for('aws-cdk-lib/aws-cloudfront/lib/origin-access-control.LambdaOriginAccessControl'); - /** - * Interface for CloudFront origin access controls + * Represents a CloudFront Origin Access Control */ export interface IOriginAccessControl extends IResource { /** @@ -14,10 +11,16 @@ export interface IOriginAccessControl extends IResource { * @attribute */ readonly originAccessControlId: string; + + /** + * The type of origin that the origin access control is for. + * @attribute + */ + readonly originAccessControlOriginType: string; } /** - * Properties for creating a OriginAccessControl resource. + * Properties for creating a Origin Access Control resource. */ export interface OriginAccessControlProps { /** @@ -48,25 +51,28 @@ export interface OriginAccessControlProps { } /** - * Origin types supported by origin access control. + * Attributes for a CloudFront Origin Access Control */ -export enum OriginAccessControlOriginType { +export interface OriginAccessControlAttributes { /** - * Uses an Amazon S3 bucket origin. - */ - S3 = 's3', - /** - * Uses an AWS Elemental MediaStore origin. + * The unique identifier of the origin access control. */ - MEDIASTORE = 'mediastore', + readonly originAccessControlId: string; + /** - * Uses a Lambda function URL origin. + * The type of origin that the origin access control is for. */ - LAMBDA = 'lambda', + readonly originAccessControlOriginType: string; +} + +/** + * Origin types supported by Origin Access Control. + */ +export enum OriginAccessControlOriginType { /** - * Uses an AWS Elemental MediaPackage v2 origin. + * Uses an Amazon S3 bucket origin. */ - MEDIAPACKAGEV2 = 'mediapackagev2', + S3 = 's3', } /** @@ -93,7 +99,7 @@ export enum SigningBehavior { } /** - * The signing protocol of the origin access control. + * The signing protocol of the Origin Access Control. */ export enum SigningProtocol { /** @@ -109,37 +115,33 @@ export enum SigningProtocol { */ export class OriginAccessControl extends Resource implements IOriginAccessControl { /** - * Imports an origin access control from its id. + * Imports an origin access control from its id and origin type. */ - public static fromOriginAccessControlId(scope: Construct, id: string, originAccessControlId: string): IOriginAccessControl { + public static fromOriginAccessControlAttributes(scope: Construct, id: string, attrs: OriginAccessControlAttributes): IOriginAccessControl { class Import extends Resource implements IOriginAccessControl { - public readonly originAccessControlId = originAccessControlId; - constructor(s: Construct, i: string) { - super(s, i); - - this.originAccessControlId = originAccessControlId; - } + public readonly originAccessControlId = attrs.originAccessControlId; + public readonly originAccessControlOriginType = attrs.originAccessControlOriginType; } return new Import(scope, id); } - public static forS3(scope: Construct, id: string, props: OriginAccessControlProps = {}) { - return new S3OriginAccessControl(scope, id, props); - } - - public static forLambda(scope: Construct, id: string, props: OriginAccessControlProps = {}) { - return new LambdaOriginAccessControl(scope, id, props); - } - /** * The unique identifier of this Origin Access Control. * @attribute */ public readonly originAccessControlId: string; + /** + * The type of origin that the origin access control is for. + * @attribute + */ + public readonly originAccessControlOriginType: string; + constructor(scope: Construct, id: string, props: OriginAccessControlProps = {}) { super(scope, id); + this.originAccessControlOriginType = props.originAccessControlOriginType ?? OriginAccessControlOriginType.S3; + const resource = new CfnOriginAccessControl(this, 'Resource', { originAccessControlConfig: { description: props.description, @@ -148,53 +150,10 @@ export class OriginAccessControl extends Resource implements IOriginAccessContro }), signingBehavior: props.signingBehavior ?? SigningBehavior.ALWAYS, signingProtocol: props.signingProtocol ?? SigningProtocol.SIGV4, - originAccessControlOriginType: props.originAccessControlOriginType ?? OriginAccessControlOriginType.S3, + originAccessControlOriginType: this.originAccessControlOriginType, }, }); this.originAccessControlId = resource.attrId; } -} - -/** - * Origin access control for a S3 bucket origin - */ -export class S3OriginAccessControl extends OriginAccessControl { - /** - * Returns `true` if `x` is an S3OriginAccessControl, `false` otherwise - */ - public static isS3OriginAccessControl(x: any): x is S3OriginAccessControl { - return x !== null && typeof (x) === 'object' && S3_ORIGIN_ACCESS_CONTROL_SYMBOL in x; - } - constructor(scope: Construct, id: string, props: OriginAccessControlProps = {}) { - super(scope, id, { ...props, originAccessControlOriginType: OriginAccessControlOriginType.S3 }); - } -} - -Object.defineProperty(S3OriginAccessControl.prototype, S3_ORIGIN_ACCESS_CONTROL_SYMBOL, { - value: true, - enumerable: false, - writable: false, -}); - -/** - * Origin access control for a Lambda Function Url origin - */ -export class LambdaOriginAccessControl extends OriginAccessControl { - /** - * Returns `true` if `x` is a LambdaOriginAccessControl, `false` otherwise - */ - public static isLambdaOriginAccessControl(x: any): x is LambdaOriginAccessControl { - return x !== null && typeof (x) === 'object' && LAMBDA_ORIGIN_ACCESS_CONTROL_SYMBOL in x; - } - - constructor(scope: Construct, id: string, props: OriginAccessControlProps = {}) { - super(scope, id, { ...props, originAccessControlOriginType: OriginAccessControlOriginType.LAMBDA }); - } -} - -Object.defineProperty(LambdaOriginAccessControl.prototype, LAMBDA_ORIGIN_ACCESS_CONTROL_SYMBOL, { - value: true, - enumerable: false, - writable: false, -}); \ No newline at end of file +} \ No newline at end of file From f09ee03d4a4689f70bf06bb312e7a577348cbf06 Mon Sep 17 00:00:00 2001 From: gracelu0 Date: Mon, 24 Jun 2024 15:19:48 -0700 Subject: [PATCH 10/14] Update feature flag to OAC default only --- .../index.ts | 9 ++- .../index.ts | 24 ++++-- ...cess-control-bucket-policy-handler.test.ts | 11 +++ ...-access-control-key-policy-handler.test.ts | 0 .../aws-cloudfront-origins/lib/s3-origin.ts | 79 ++++++++++++++++--- .../lib/origin-access-control.ts | 13 ++- .../aws-cdk-lib/aws-cloudfront/lib/origin.ts | 2 +- .../aws-cloudfront/lib/web-distribution.ts | 42 ---------- .../test/origin-access-control.test.ts | 67 ++++++++++++++++ packages/aws-cdk-lib/cx-api/FEATURE_FLAGS.md | 25 +----- packages/aws-cdk-lib/cx-api/lib/features.ts | 8 +- 11 files changed, 189 insertions(+), 91 deletions(-) create mode 100644 packages/@aws-cdk/custom-resource-handlers/test/aws-cloudfront-origins/s3-origin-access-control-bucket-policy-handler.test.ts create mode 100644 packages/@aws-cdk/custom-resource-handlers/test/aws-cloudfront-origins/s3-origin-access-control-key-policy-handler.test.ts diff --git a/packages/@aws-cdk/custom-resource-handlers/lib/aws-cloudfront-origins/s3-origin-access-control-bucket-policy-handler/index.ts b/packages/@aws-cdk/custom-resource-handlers/lib/aws-cloudfront-origins/s3-origin-access-control-bucket-policy-handler/index.ts index 77117b1c546cd..b64b6c1ad11dd 100644 --- a/packages/@aws-cdk/custom-resource-handlers/lib/aws-cloudfront-origins/s3-origin-access-control-bucket-policy-handler/index.ts +++ b/packages/@aws-cdk/custom-resource-handlers/lib/aws-cloudfront-origins/s3-origin-access-control-bucket-policy-handler/index.ts @@ -11,6 +11,7 @@ interface updateBucketPolicyProps { distributionId: string; partition: string; accountId: string; + actions: string[]; } export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent) { @@ -21,8 +22,9 @@ export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent const accountId = props.AccountId; const partition = props.Partition; const bucketName = props.BucketName; + const actions = props.Actions; - await updateBucketPolicy({ bucketName, distributionId, partition, accountId }); + await updateBucketPolicy({ bucketName, distributionId, partition, accountId, actions }); return { IsComplete: true, @@ -39,13 +41,14 @@ async function updateBucketPolicy(props: updateBucketPolicyProps) { const prevPolicyJson = (await s3.getBucketPolicy({ Bucket: props.bucketName }))?.Policy ?? S3_POLICY_STUB; const policy = JSON.parse(prevPolicyJson); console.log('Previous bucket policy:', JSON.stringify(policy, undefined, 2)); + const oacBucketPolicyStatement = { - Sid: 'AllowS3OACAccess', + Sid: 'GrantOACAccessToS3', Principal: { Service: ['cloudfront.amazonaws.com'], }, Effect: 'Allow', - Action: ['s3:GetObject'], + Action: props.actions, Resource: [`arn:${props.partition}:s3:::${props.bucketName}/*`], Condition: { StringEquals: { diff --git a/packages/@aws-cdk/custom-resource-handlers/lib/aws-cloudfront-origins/s3-origin-access-control-key-policy-handler/index.ts b/packages/@aws-cdk/custom-resource-handlers/lib/aws-cloudfront-origins/s3-origin-access-control-key-policy-handler/index.ts index 8dfb15b79adcd..102783b3e1b49 100644 --- a/packages/@aws-cdk/custom-resource-handlers/lib/aws-cloudfront-origins/s3-origin-access-control-key-policy-handler/index.ts +++ b/packages/@aws-cdk/custom-resource-handlers/lib/aws-cloudfront-origins/s3-origin-access-control-key-policy-handler/index.ts @@ -4,6 +4,11 @@ import { KMS, KeyManagerType } from '@aws-sdk/client-kms'; const kms = new KMS({}); +const KEY_ACTIONS: Record = { + READ: ['kms:Decrypt'], + WRITE: ['kms:Encrypt', 'kms:GenerateDataKey*'], +}; + export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent) { if (event.RequestType === 'Create' || event.RequestType === 'Update') { @@ -13,6 +18,7 @@ export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent const accountId = props.AccountId; const partition = props.Partition; const region = process.env.AWS_REGION; + const accessLevels = props.AccessLevels; const describeKeyCommandResponse = await kms.describeKey({ KeyId: kmsKeyId, @@ -38,6 +44,9 @@ export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent // Define the updated key policy to allow CloudFront Distribution access const keyPolicy = JSON.parse(getKeyPolicyCommandResponse?.Policy); console.log('Retrieved key policy', JSON.stringify(keyPolicy, undefined, 2)); + + const actions = getActions(accessLevels); + const kmsKeyPolicyStatement = { Sid: 'AllowCloudFrontServicePrincipalSSE-KMS', Effect: 'Allow', @@ -46,11 +55,7 @@ export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent 'cloudfront.amazonaws.com', ], }, - Action: [ - 'kms:Decrypt', - 'kms:Encrypt', - 'kms:GenerateDataKey*', - ], + Action: actions, Resource: `arn:${partition}:kms:${region}:${accountId}:key/${kmsKeyId}`, Condition: { StringEquals: { @@ -58,6 +63,7 @@ export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent }, }, }; + const updatedKeyPolicy = updatePolicy(keyPolicy, kmsKeyPolicyStatement); console.log('Updated key policy', JSON.stringify(updatedKeyPolicy, undefined, 2)); await kms.putKeyPolicy({ @@ -74,6 +80,14 @@ export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent } } +function getActions(accessLevels: string[]): string[] { + let actions: string[] = []; + for (const accessLevel of accessLevels) { + actions = actions.concat(KEY_ACTIONS[accessLevel]); + } + return actions; +} + /** * Updates a provided policy with a provided policy statement. First checks whether the provided policy statement * already exists. If an existing policy is found with a matching sid, the provided policy will overwrite the existing diff --git a/packages/@aws-cdk/custom-resource-handlers/test/aws-cloudfront-origins/s3-origin-access-control-bucket-policy-handler.test.ts b/packages/@aws-cdk/custom-resource-handlers/test/aws-cloudfront-origins/s3-origin-access-control-bucket-policy-handler.test.ts new file mode 100644 index 0000000000000..cc4c6d68a0d1e --- /dev/null +++ b/packages/@aws-cdk/custom-resource-handlers/test/aws-cloudfront-origins/s3-origin-access-control-bucket-policy-handler.test.ts @@ -0,0 +1,11 @@ +import { S3Client, GetBucketPolicyCommand, PutBucketPolicyCommand } from '@aws-sdk/client-s3'; +import { mockClient } from 'aws-sdk-client-mock'; +import 'aws-sdk-client-mock-jest'; +// import { handler } from '../../lib/aws-cloudfront-origins/s3-origin-access-control-key-policy-handler/index'; + + +const s3Mock = mockClient(S3Client); +beforeEach(() => { + s3Mock.reset(); +}); + diff --git a/packages/@aws-cdk/custom-resource-handlers/test/aws-cloudfront-origins/s3-origin-access-control-key-policy-handler.test.ts b/packages/@aws-cdk/custom-resource-handlers/test/aws-cloudfront-origins/s3-origin-access-control-key-policy-handler.test.ts new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/packages/aws-cdk-lib/aws-cloudfront-origins/lib/s3-origin.ts b/packages/aws-cdk-lib/aws-cloudfront-origins/lib/s3-origin.ts index d4e00c5ad1579..bb7b9e95f8d01 100644 --- a/packages/aws-cdk-lib/aws-cloudfront-origins/lib/s3-origin.ts +++ b/packages/aws-cdk-lib/aws-cloudfront-origins/lib/s3-origin.ts @@ -9,8 +9,15 @@ import { S3OriginAccessControlBucketPolicyProvider } from '../../custom-resource import { S3OriginAccessControlKeyPolicyProvider } from '../../custom-resource-handlers/dist/aws-cloudfront-origins/s3-origin-access-control-key-policy-provider.generated'; import * as cxapi from '../../cx-api'; -const S3_ORIGIN_ACCESS_CONTROL_KEY_RESOURCE_TYPE = 'Custom::S3OriginAccessControlKeyPolicy'; -const S3_ORIGIN_ACCESS_CONTROL_BUCKET_RESOURCE_TYPE = 'Custom::S3OriginAccessControlBucketPolicy'; +const S3_ORIGIN_ACCESS_CONTROL_KEY_RESOURCE_TYPE = 'Custom::S3OriginAccessControlKeyPolicyUpdater'; +const S3_ORIGIN_ACCESS_CONTROL_BUCKET_RESOURCE_TYPE = 'Custom::S3OriginAccessControlBucketPolicyUpdater'; + +const BUCKET_ACTIONS: Record = { + READ: ['s3:GetObject'], + WRITE: ['s3:PutObject'], + DELETE: ['s3:DeleteObject'] +}; + /** * Properties to use to customize an S3 Origin. */ @@ -29,11 +36,36 @@ export interface S3OriginProps extends cloudfront.OriginProps { readonly originAccessControl?: cloudfront.IOriginAccessControl; /** - * When set to 'true', an attempt will be made to update the bucket policy to allow the + * When set to 'true', a best-effort attempt will be made to update the bucket policy to allow the * CloudFront distribution access. * @default false */ readonly overrideImportedBucketPolicy?: boolean; + + /** + * The level of permissions granted in the bucket policy and key policy (if applicable) + * to the CloudFront distribution. + * @default AccessLevel.READ + */ + readonly originAccessLevels?: AccessLevel[]; +} + +/** + * The types of permissions to grant OAC access to th S3 origin + */ +export enum AccessLevel { + /** + * Grants 's3:GetObject' permission to OAC + */ + READ = 'READ', + /** + * Grants 's3:PutObject' permission to OAC + */ + WRITE = 'WRITE', + /** + * Grants 's3:DeleteObject' permission to OAC + */ + DELETE = 'DELETE', } /** @@ -56,10 +88,12 @@ export class S3Origin implements cloudfront.IOrigin { protocolPolicy: cloudfront.OriginProtocolPolicy.HTTP_ONLY, // S3 only supports HTTP for website buckets ...props, }); - } else if (props.originAccessIdentity || !FeatureFlags.of(bucket.stack).isEnabled(cxapi.CLOUDFRONT_USE_ORIGIN_ACCESS_CONTROL)) { + } else if (props.originAccessControl) { + this.origin = S3BucketOrigin.withAccessControl(bucket, props); + } else if (props.originAccessIdentity) { this.origin = S3BucketOrigin.withAccessIdentity(bucket, props); } else { - this.origin = S3BucketOrigin.withAccessControl(bucket, props); + this.origin = FeatureFlags.of(bucket.stack).isEnabled(cxapi.CLOUDFRONT_USE_ORIGIN_ACCESS_CONTROL_BY_DEFAULT) ? S3BucketOrigin.withAccessControl(bucket, props) : S3BucketOrigin.withAccessIdentity(bucket, props); } } @@ -139,12 +173,13 @@ abstract class S3BucketOrigin extends cloudfront.OriginBase { } const distributionId = options.distributionId; - const result = this.grantDistributionAccessToBucket(distributionId); + const actions = this.getActions(props.originAccessLevels ?? [AccessLevel.READ]); + const result = this.grantDistributionAccessToBucket(distributionId, actions); // Failed to update bucket policy, assume using imported bucket if (!result.statementAdded) { if (props.overrideImportedBucketPolicy) { - this.grantDistributionAccessToImportedBucket(scope, distributionId); + this.grantDistributionAccessToImportedBucket(scope, distributionId, actions); } else { Annotations.of(scope).addWarningV2('@aws-cdk/aws-cloudfront-origins:updateBucketPolicy', 'Cannot update bucket policy of an imported bucket. Set overrideImportedBucketPolicy to true or update the policy manually instead.'); @@ -152,7 +187,12 @@ abstract class S3BucketOrigin extends cloudfront.OriginBase { } if (this.bucket.encryptionKey) { - this.grantDistributionAccessToKey(scope, distributionId, this.bucket.encryptionKey); + this.grantDistributionAccessToKey( + scope, + distributionId, + this.bucket.encryptionKey, + props.originAccessLevels ?? [AccessLevel.READ], + ); } const originBindConfig = this._bind(scope, options); @@ -175,13 +215,21 @@ abstract class S3BucketOrigin extends cloudfront.OriginBase { return { originAccessIdentity: '' }; } - private grantDistributionAccessToBucket(distributionId: string): iam.AddToResourcePolicyResult { + private getActions(accessLevels: AccessLevel[]): string[] { + let actions: string[] = []; + for (const accessLevel of new Set(accessLevels)) { + actions = actions.concat(BUCKET_ACTIONS[accessLevel]); + } + return actions; + } + + private grantDistributionAccessToBucket(distributionId: string | undefined, actions: string[]): iam.AddToResourcePolicyResult { const oacReadOnlyBucketPolicyStatement = new iam.PolicyStatement( { - sid: 'AllowS3OACAccess', + sid: 'GrantOACAccessToS3', effect: iam.Effect.ALLOW, principals: [new iam.ServicePrincipal('cloudfront.amazonaws.com')], - actions: ['s3:GetObject'], + actions, resources: [this.bucket.arnForObjects('*')], conditions: { StringEquals: { @@ -197,7 +245,7 @@ abstract class S3BucketOrigin extends cloudfront.OriginBase { /** * Use custom resource to update bucket policy and remove OAI policy statement if it exists */ - private grantDistributionAccessToImportedBucket(scope: Construct, distributionId: string) { + private grantDistributionAccessToImportedBucket(scope: Construct, distributionId: string | undefined, actions: string[]) { const provider = S3OriginAccessControlBucketPolicyProvider.getOrCreateProvider(scope, S3_ORIGIN_ACCESS_CONTROL_BUCKET_RESOURCE_TYPE, { description: 'Lambda function that updates S3 bucket policy to allow CloudFront distribution access.', }); @@ -215,6 +263,7 @@ abstract class S3BucketOrigin extends cloudfront.OriginBase { AccountId: this.bucket.env.account, Partition: Stack.of(scope).partition, BucketName: this.bucket.bucketName, + Actions: actions, }, }); } @@ -222,7 +271,7 @@ abstract class S3BucketOrigin extends cloudfront.OriginBase { /** * Use custom resource to update KMS key policy */ - private grantDistributionAccessToKey(scope: Construct, distributionId: string, key: IKey) { + private grantDistributionAccessToKey(scope: Construct, distributionId: string | undefined, key: IKey, accessLevels: AccessLevel[]) { const provider = S3OriginAccessControlKeyPolicyProvider.getOrCreateProvider(scope, S3_ORIGIN_ACCESS_CONTROL_KEY_RESOURCE_TYPE, { description: 'Lambda function that updates SSE-KMS key policy to allow CloudFront distribution access.', @@ -233,6 +282,9 @@ abstract class S3BucketOrigin extends cloudfront.OriginBase { Resource: [key.keyArn], }); + // Remove duplicates and DELETE permissions which are not applicable to KMS key actions + const keyAccessLevels = [...new Set(accessLevels.filter(level => level !== AccessLevel.DELETE))]; + new CustomResource(scope, 'S3OriginKMSKeyPolicyCustomResource', { resourceType: S3_ORIGIN_ACCESS_CONTROL_KEY_RESOURCE_TYPE, serviceToken: provider.serviceToken, @@ -241,6 +293,7 @@ abstract class S3BucketOrigin extends cloudfront.OriginBase { KmsKeyId: key.keyId, AccountId: this.bucket.env.account, Partition: Stack.of(scope).partition, + AccessLevels: keyAccessLevels }, }); } diff --git a/packages/aws-cdk-lib/aws-cloudfront/lib/origin-access-control.ts b/packages/aws-cdk-lib/aws-cloudfront/lib/origin-access-control.ts index 203c376b90e8f..9fc74e1c81629 100644 --- a/packages/aws-cdk-lib/aws-cloudfront/lib/origin-access-control.ts +++ b/packages/aws-cdk-lib/aws-cloudfront/lib/origin-access-control.ts @@ -1,6 +1,6 @@ import { Construct } from 'constructs'; import { CfnOriginAccessControl } from './cloudfront.generated'; -import { IResource, Resource, Names } from '../../core'; +import { IResource, Resource, Names, Token } from '../../core'; /** * Represents a CloudFront Origin Access Control @@ -142,6 +142,11 @@ export class OriginAccessControl extends Resource implements IOriginAccessContro this.originAccessControlOriginType = props.originAccessControlOriginType ?? OriginAccessControlOriginType.S3; + // check if origin access control name is 64 characters or less + if (props.originAccessControlName) { + this.validateOriginAccessControlName(props.originAccessControlName); + } + const resource = new CfnOriginAccessControl(this, 'Resource', { originAccessControlConfig: { description: props.description, @@ -156,4 +161,10 @@ export class OriginAccessControl extends Resource implements IOriginAccessContro this.originAccessControlId = resource.attrId; } + + private validateOriginAccessControlName(name: string) { + if (!Token.isUnresolved(name) && name.length > 64) { + throw new Error(`Origin access control name must be 64 characters or less, '${name}' has length ${name.length}`); + } + } } \ No newline at end of file diff --git a/packages/aws-cdk-lib/aws-cloudfront/lib/origin.ts b/packages/aws-cdk-lib/aws-cloudfront/lib/origin.ts index 049e9bb80a56a..5bc169dcbcbb0 100644 --- a/packages/aws-cdk-lib/aws-cloudfront/lib/origin.ts +++ b/packages/aws-cdk-lib/aws-cloudfront/lib/origin.ts @@ -130,7 +130,7 @@ export interface OriginBindOptions { /** * The identifier of the Distribution this Origin is used for. */ - readonly distributionId: string; + readonly distributionId?: string | undefined; } /** diff --git a/packages/aws-cdk-lib/aws-cloudfront/lib/web-distribution.ts b/packages/aws-cdk-lib/aws-cloudfront/lib/web-distribution.ts index fe60b6163a867..6d512ebe4f3be 100644 --- a/packages/aws-cdk-lib/aws-cloudfront/lib/web-distribution.ts +++ b/packages/aws-cdk-lib/aws-cloudfront/lib/web-distribution.ts @@ -4,7 +4,6 @@ import { HttpVersion, IDistribution, LambdaEdgeEventType, OriginProtocolPolicy, import { FunctionAssociation } from './function'; import { GeoRestriction } from './geo-restriction'; import { IKeyGroup } from './key-group'; -import { IOriginAccessControl } from './origin-access-control'; import { IOriginAccessIdentity } from './origin-access-identity'; import { formatDistributionArn } from './private/utils'; import * as certificatemanager from '../../aws-certificatemanager'; @@ -215,12 +214,6 @@ export interface SourceConfiguration { * @default - origin shield not enabled */ readonly originShieldRegion?: string; - - /** - * Origin Access Control - * @default - No origin access control - */ - readonly originAccessControl?: IOriginAccessControl; } /** @@ -319,13 +312,6 @@ export interface S3OriginConfig { */ readonly originAccessIdentity?: IOriginAccessIdentity; - /** - * The optional Origin Access Control that Cloudfront will use when accessing your S3 bucket. - * - * @default - No origin access control - */ - readonly originAccessControl?: IOriginAccessControl; - /** * The relative path to the origin root to use for sources. * @@ -1134,9 +1120,6 @@ export class CloudFrontWebDistribution extends cdk.Resource implements IDistribu let s3OriginConfig: CfnDistribution.S3OriginConfigProperty | undefined; if (originConfig.s3OriginSource) { - if (originConfig.s3OriginSource.originAccessIdentity && originConfig.s3OriginSource.originAccessControl) { - throw Error('Only one of origin access identity or origin access control can be defined.'); - } // first case for backwards compatibility if (originConfig.s3OriginSource.originAccessIdentity) { // grant CloudFront OriginAccessIdentity read access to S3 bucket @@ -1153,30 +1136,6 @@ export class CloudFrontWebDistribution extends cdk.Resource implements IDistribu s3OriginConfig = { originAccessIdentity: `origin-access-identity/cloudfront/${originConfig.s3OriginSource.originAccessIdentity.originAccessIdentityId}`, }; - } else if (originConfig.s3OriginSource.originAccessControl) { - const oacReadOnlyBucketPolicyStatement = new iam.PolicyStatement( - { - sid: 'AllowS3OACAccess', - effect: iam.Effect.ALLOW, - principals: [new iam.ServicePrincipal('cloudfront.amazonaws.com')], - actions: ['s3:GetObject'], - resources: [originConfig.s3OriginSource.s3BucketSource.arnForObjects('*')], - conditions: { - StringEquals: { - 'AWS:SourceArn': formatDistributionArn(this), - }, - }, - }, - ); - const result = originConfig.s3OriginSource.s3BucketSource.addToResourcePolicy(oacReadOnlyBucketPolicyStatement); - - if (!result.statementAdded) { - Annotations.of(this).addWarningV2('@aws-cdk/aws-cloudfront:webDistribution', 'Cannot update bucket policy of an imported bucket. Update the policy manually instead.'); - } - - s3OriginConfig = { - originAccessIdentity: '', - }; } else { s3OriginConfig = {}; } @@ -1201,7 +1160,6 @@ export class CloudFrontWebDistribution extends cdk.Resource implements IDistribu originCustomHeaders: originHeaders.length > 0 ? originHeaders : undefined, s3OriginConfig, - originAccessControlId: originConfig.s3OriginSource?.originAccessControl?.originAccessControlId, originShield: this.toOriginShieldProperty(originConfig), customOriginConfig: originConfig.customOriginSource ? { diff --git a/packages/aws-cdk-lib/aws-cloudfront/test/origin-access-control.test.ts b/packages/aws-cdk-lib/aws-cloudfront/test/origin-access-control.test.ts index e69de29bb2d1d..a89465be6b508 100644 --- a/packages/aws-cdk-lib/aws-cloudfront/test/origin-access-control.test.ts +++ b/packages/aws-cdk-lib/aws-cloudfront/test/origin-access-control.test.ts @@ -0,0 +1,67 @@ +import { Template } from '../../assertions'; +import { Stack } from '../../core'; +import { OriginAccessControl, SigningBehavior, SigningProtocol, OriginAccessControlOriginType } from '../lib'; + +describe('OriginAccessControl', () => { + let stack: Stack; + + beforeEach(() => { + stack = new Stack(); + }); + + test('creates an OriginAccessControl with default properties', () => { + new OriginAccessControl(stack, 'TestOriginAccessControl'); + + Template.fromStack(stack).resourceCountIs('AWS::CloudFront::OriginAccessControl', 1); + }); + + test('creates an OriginAccessControl with custom properties', () => { + const description = 'Test description'; + const originAccessControlName = 'TestOriginAccessControl'; + const signingBehavior = SigningBehavior.NEVER; + const signingProtocol = SigningProtocol.SIGV4; + const originAccessControlOriginType = OriginAccessControlOriginType.S3; + + new OriginAccessControl(stack, 'TestOriginAccessControl', { + description, + originAccessControlName, + signingBehavior, + signingProtocol, + originAccessControlOriginType, + }); + + const template = Template.fromStack(stack); + template.resourceCountIs('AWS::CloudFront::OriginAccessControl', 1); + template.hasResourceProperties('AWS::CloudFront::OriginAccessControl', { + OriginAccessControlConfig: { + Description: description, + Name: originAccessControlName, + SigningBehavior: signingBehavior, + SigningProtocol: signingProtocol, + OriginAccessControlOriginType: originAccessControlOriginType, + }, + }); + }); + + test('imports an OriginAccessControl from attributes', () => { + const originAccessControlId = 'ABC123ABC123AB'; + const originAccessControlOriginType = OriginAccessControlOriginType.S3; + + const imported = OriginAccessControl.fromOriginAccessControlAttributes(stack, 'ImportedOriginAccessControl', { + originAccessControlId, + originAccessControlOriginType, + }); + + expect(imported.originAccessControlId).toEqual(originAccessControlId); + expect(imported.originAccessControlOriginType).toEqual(originAccessControlOriginType); + }); + + test('throws an error when originAccessControlName is too long', () => { + const longName = 'a'.repeat(65); + expect(() => { + new OriginAccessControl(stack, 'TestOriginAccessControl', { + originAccessControlName: longName, + }); + }).toThrow(`Origin access control name must be 64 characters or less, '${longName}' has length 65`); + }); +}); \ No newline at end of file diff --git a/packages/aws-cdk-lib/cx-api/FEATURE_FLAGS.md b/packages/aws-cdk-lib/cx-api/FEATURE_FLAGS.md index 2f4e05544d91a..4b6e50325aee5 100644 --- a/packages/aws-cdk-lib/cx-api/FEATURE_FLAGS.md +++ b/packages/aws-cdk-lib/cx-api/FEATURE_FLAGS.md @@ -71,17 +71,8 @@ Flags come in three types: | [@aws-cdk/aws-ec2:ebsDefaultGp3Volume](#aws-cdkaws-ec2ebsdefaultgp3volume) | When enabled, the default volume type of the EBS volume will be GP3 | 2.140.0 | (default) | | [@aws-cdk/pipelines:reduceAssetRoleTrustScope](#aws-cdkpipelinesreduceassetroletrustscope) | Remove the root account principal from PipelineAssetsFileRole trust policy | 2.141.0 | (default) | | [@aws-cdk/aws-ecs:removeDefaultDeploymentAlarm](#aws-cdkaws-ecsremovedefaultdeploymentalarm) | When enabled, remove default deployment alarm settings | 2.143.0 | (default) | -<<<<<<< HEAD -<<<<<<< HEAD | [@aws-cdk/custom-resources:logApiResponseDataPropertyTrueDefault](#aws-cdkcustom-resourceslogapiresponsedatapropertytruedefault) | When enabled, the custom resource used for `AwsCustomResource` will configure the `logApiResponseData` property as true by default | 2.145.0 | (fix) | -======= -======= -| [@aws-cdk/aws-cloudfront:useOriginAccessControl](#aws-cdkaws-cloudfrontuseoriginaccesscontrol) | When enabled, use Origin Access Control rather than Origin Access Identity | V2NEXT | (fix) | ->>>>>>> 386904900a (wip oac) ->>>>>>> c47258bd5d (wip oac) -======= | [@aws-cdk/aws-cloudfront:useOriginAccessControl](#aws-cdkaws-cloudfrontuseoriginaccesscontrol) | When enabled, an origin access control will be created automatically when a new S3 origin is created. | V2NEXT | (fix) | ->>>>>>> a76e6cc968 (refactor) @@ -142,21 +133,9 @@ The following json shows the current recommended set of flags, as `cdk init` wou "@aws-cdk/aws-kms:reduceCrossAccountRegionPolicyScope": true, "@aws-cdk/aws-eks:nodegroupNameAttribute": true, "@aws-cdk/aws-ec2:ebsDefaultGp3Volume": true, -<<<<<<< HEAD -<<<<<<< HEAD - "@aws-cdk/aws-ecs:removeDefaultDeploymentAlarm": true, - "@aws-cdk/custom-resources:logApiResponseDataPropertyTrueDefault": false -======= -<<<<<<< HEAD - "@aws-cdk/aws-ecs:removeDefaultDeploymentAlarm": true -======= - "@aws-cdk/aws-cloudfront:useOriginAccessControl": true, ->>>>>>> 386904900a (wip oac) ->>>>>>> c47258bd5d (wip oac) -======= "@aws-cdk/aws-ecs:removeDefaultDeploymentAlarm": true, + "@aws-cdk/custom-resources:logApiResponseDataPropertyTrueDefault": false, "@aws-cdk/aws-cloudfront:useOriginAccessControl": true ->>>>>>> a76e6cc968 (refactor) } } ``` @@ -1372,6 +1351,7 @@ Unlike most feature flags, we don't recommend setting this feature flag to true. the event object, then setting this feature flag will keep this behavior. Otherwise, setting this feature flag to false will trigger an 'Update' event by removing the 'logApiResponseData' property from the event object. + | Since | Default | Recommended | | ----- | ----- | ----- | | (not in v1) | | | @@ -1385,6 +1365,7 @@ property from the event object. When this feature flag is enabled, an origin access control will be created automatically when a new `S3Origin` is created instead of an origin access identity (legacy). + | Since | Default | Recommended | | ----- | ----- | ----- | | (not in v1) | | | diff --git a/packages/aws-cdk-lib/cx-api/lib/features.ts b/packages/aws-cdk-lib/cx-api/lib/features.ts index 46c159ef4435c..1e9439ed91701 100644 --- a/packages/aws-cdk-lib/cx-api/lib/features.ts +++ b/packages/aws-cdk-lib/cx-api/lib/features.ts @@ -107,7 +107,7 @@ export const EKS_NODEGROUP_NAME = '@aws-cdk/aws-eks:nodegroupNameAttribute'; export const EBS_DEFAULT_GP3 = '@aws-cdk/aws-ec2:ebsDefaultGp3Volume'; export const ECS_REMOVE_DEFAULT_DEPLOYMENT_ALARM = '@aws-cdk/aws-ecs:removeDefaultDeploymentAlarm'; export const LOG_API_RESPONSE_DATA_PROPERTY_TRUE_DEFAULT = '@aws-cdk/custom-resources:logApiResponseDataPropertyTrueDefault'; -export const CLOUDFRONT_USE_ORIGIN_ACCESS_CONTROL = '@aws-cdk/aws-cloudfront:useOriginAccessControl'; +export const CLOUDFRONT_USE_ORIGIN_ACCESS_CONTROL_BY_DEFAULT = '@aws-cdk/aws-cloudfront:useOriginAccessControlByDefault'; export const FLAGS: Record = { ////////////////////////////////////////////////////////////////////// @@ -1124,11 +1124,11 @@ export const FLAGS: Record = { }, ////////////////////////////////////////////////////////////////////// - [CLOUDFRONT_USE_ORIGIN_ACCESS_CONTROL]: { + [CLOUDFRONT_USE_ORIGIN_ACCESS_CONTROL_BY_DEFAULT]: { type: FlagType.BugFix, - summary: 'When enabled, an origin access control will be created automatically when a new S3 origin is created.', + summary: 'When enabled, an origin access control will be created by default when a new S3 origin is created.', detailsMd: ` - When this feature flag is enabled, an origin access control will be created automatically when a new \`S3Origin\` is created instead + When this feature flag is enabled, an origin access control will be created by default when a new \`S3Origin\` is created instead of an origin access identity (legacy). `, introducedIn: { v2: 'V2NEXT' }, From 540e1b6674b68eb16e4962d86e7765a4eb1dd803 Mon Sep 17 00:00:00 2001 From: gracelu0 Date: Tue, 25 Jun 2024 03:29:56 -0700 Subject: [PATCH 11/14] Add unit tests --- .../index.ts | 127 +++++++--- .../index.ts | 72 ++++-- ...cess-control-bucket-policy-handler.test.ts | 230 +++++++++++++++++- ...-access-control-key-policy-handler.test.ts | 196 +++++++++++++++ .../aws-cloudfront-origins/lib/s3-origin.ts | 2 +- .../test/s3-origin.test.ts | 163 ++++++++++++- 6 files changed, 722 insertions(+), 68 deletions(-) diff --git a/packages/@aws-cdk/custom-resource-handlers/lib/aws-cloudfront-origins/s3-origin-access-control-bucket-policy-handler/index.ts b/packages/@aws-cdk/custom-resource-handlers/lib/aws-cloudfront-origins/s3-origin-access-control-bucket-policy-handler/index.ts index b64b6c1ad11dd..521c96cabb0cf 100644 --- a/packages/@aws-cdk/custom-resource-handlers/lib/aws-cloudfront-origins/s3-origin-access-control-bucket-policy-handler/index.ts +++ b/packages/@aws-cdk/custom-resource-handlers/lib/aws-cloudfront-origins/s3-origin-access-control-bucket-policy-handler/index.ts @@ -3,7 +3,7 @@ import { S3 } from '@aws-sdk/client-s3'; const S3_POLICY_STUB = JSON.stringify({ Version: '2012-10-17', Statement: [] }); - +const S3_OAC_POLICY_SID = 'GrantOACAccessToS3'; const s3 = new S3({}); interface updateBucketPolicyProps { @@ -15,26 +15,33 @@ interface updateBucketPolicyProps { } export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent) { + const props = event.ResourceProperties; + const distributionId = props.DistributionId; + const accountId = props.AccountId; + const partition = props.Partition; + const bucketName = props.BucketName; + const actions = props.Actions; if (event.RequestType === 'Create' || event.RequestType === 'Update') { - const props = event.ResourceProperties; - const distributionId = props.DistributionId; - const accountId = props.AccountId; - const partition = props.Partition; - const bucketName = props.BucketName; - const actions = props.Actions; - await updateBucketPolicy({ bucketName, distributionId, partition, accountId, actions }); return { IsComplete: true, }; - } else { - return; + } else if (event.RequestType === 'Delete') { + await removeOacPolicyStatement( + bucketName, + distributionId, + partition, + accountId + ) + return { + IsComplete: true, + }; } } -async function updateBucketPolicy(props: updateBucketPolicyProps) { +export async function updateBucketPolicy(props: updateBucketPolicyProps) { // make API calls to update bucket policy try { console.log('calling getBucketPolicy...'); @@ -43,7 +50,7 @@ async function updateBucketPolicy(props: updateBucketPolicyProps) { console.log('Previous bucket policy:', JSON.stringify(policy, undefined, 2)); const oacBucketPolicyStatement = { - Sid: 'GrantOACAccessToS3', + Sid: S3_OAC_POLICY_SID, Principal: { Service: ['cloudfront.amazonaws.com'], }, @@ -66,14 +73,8 @@ async function updateBucketPolicy(props: updateBucketPolicyProps) { }); // Check if policy has OAI principal and remove - updatedBucketPolicy.Statement = updatedBucketPolicy.Statement.filter((statement: any) => !isOaiPrincipal(statement)); + await removeOaiPolicyStatements(updatedBucketPolicy, props.bucketName); - await s3.putBucketPolicy({ - Bucket: props.bucketName, - Policy: JSON.stringify(updatedBucketPolicy), - }); - - console.log('Updated bucket policy to remove OAI principal policy statement:', JSON.stringify(updatedBucketPolicy, undefined, 2)); } catch (error: any) { console.log(error); if (error.name === 'NoSuchBucket') { @@ -86,24 +87,13 @@ async function updateBucketPolicy(props: updateBucketPolicyProps) { /** * Updates a provided policy with a provided policy statement. First checks whether the provided policy statement - * already exists. If an existing policy is found with a matching sid, the provided policy will overwrite the existing - * policy. If no matching policy is found, the provided policy will be appended onto the array of policy statements. + * already exists. If an existing policy is found, there will be no operation. If no matching policy + * is found, the provided policy will be appended onto the array of policy statements. * @param currentPolicy - the JSON.parse'd result of the otherwise stringified policy. * @param policyStatementToAdd - the policy statement to be added to the policy. * @returns currentPolicy - the updated policy. */ -function updatePolicy(currentPolicy: any, policyStatementToAdd: any) { - // // Check to see if a duplicate key policy exists by matching on the sid. This is to prevent duplicate key policies - // // from being added/updated in response to a stack being updated one or more times after initial creation. - // const existingPolicyIndex = currentPolicy.Statement.findIndex((statement: any) => statement.Sid === policyStatementToAdd.Sid); - // // If a match is found, overwrite the key policy statement... - // // Otherwise, push the new key policy to the array of statements - // if (existingPolicyIndex > -1) { - // currentPolicy.Statement[existingPolicyIndex] = policyStatementToAdd; - // } else { - // currentPolicy.Statement.push(policyStatementToAdd); - // } - +export function updatePolicy(currentPolicy: any, policyStatementToAdd: any) { if (!isStatementInPolicy(currentPolicy, policyStatementToAdd)) { currentPolicy.Statement.push(policyStatementToAdd); } @@ -112,14 +102,14 @@ function updatePolicy(currentPolicy: any, policyStatementToAdd: any) { return currentPolicy; }; -function isStatementInPolicy(policy: any, statement: any): boolean { +export function isStatementInPolicy(policy: any, statement: any): boolean { return policy.Statement.some((existingStatement: any) => JSON.stringify(existingStatement) === JSON.stringify(statement)); } /** * Check if the policy contains an OAI principal */ -function isOaiPrincipal(statement: any) { +export function isOaiPrincipal(statement: any) { if (statement.Principal && statement.Principal.AWS) { const principal = statement.Principal.AWS; if (typeof principal === 'string' && principal.includes('cloudfront:user/CloudFront Origin Access Identity')) { @@ -127,4 +117,69 @@ function isOaiPrincipal(statement: any) { } } return false; -} \ No newline at end of file +} + +export async function removeOaiPolicyStatements(bucketPolicy: any, bucketName: string) { + const currentPolicyStatementLength = bucketPolicy.Statement.length; + const filteredPolicyStatement = bucketPolicy.Statement.filter((statement: any) => !isOaiPrincipal(statement)); + + if (currentPolicyStatementLength !== filteredPolicyStatement.length) { + bucketPolicy.Statement = filteredPolicyStatement; + await s3.putBucketPolicy({ + Bucket: bucketName, + Policy: JSON.stringify(bucketPolicy), + }); + } + console.log('Updated bucket policy to remove OAI principal policy statement:', JSON.stringify(bucketPolicy, undefined, 2)); +} + +export async function removeOacPolicyStatement(bucketName: string, distributionId: string, partition: string, accountId: string) { + try { + console.log('calling getBucketPolicy...'); + const prevPolicyJson = (await s3.getBucketPolicy({ Bucket: bucketName }))?.Policy; + + // Return if bucket does not have a policy + if (!prevPolicyJson) { + return; + } + + const policy = JSON.parse(prevPolicyJson); + console.log('Previous bucket policy:', JSON.stringify(policy, undefined, 2)); + + const updatedBucketPolicy = { + ...policy, + Statement: policy.Statement.filter((statement: any) => !isOacPolicyStatement( + statement, + distributionId, + partition, + accountId + )), + }; + + console.log('Updated bucket policy', JSON.stringify(updatedBucketPolicy, undefined, 2)); + + await s3.putBucketPolicy({ + Bucket: bucketName, + Policy: JSON.stringify(updatedBucketPolicy), + }); + } catch (error: any) { + console.log(error); + if (error.name === 'NoSuchBucket') { + throw error; // Rethrow for further logging/handling up the stack + } + + console.log(`Could not remove origin access control policy from bucket '${bucketName}'.`); + } +} + +export function isOacPolicyStatement(statement: any, distributionId: string, partition: string, accountId: string): boolean { + return ( + statement.Sid === S3_OAC_POLICY_SID && + statement.Principal && + statement.Principal.Service && + statement.Principal.Service.includes('cloudfront.amazonaws.com') && + statement.Condition && + statement.Condition.StringEquals && + statement.Condition.StringEquals['AWS:SourceArn'] === `arn:${partition}:cloudfront::${accountId}:distribution/${distributionId}` + ); +} diff --git a/packages/@aws-cdk/custom-resource-handlers/lib/aws-cloudfront-origins/s3-origin-access-control-key-policy-handler/index.ts b/packages/@aws-cdk/custom-resource-handlers/lib/aws-cloudfront-origins/s3-origin-access-control-key-policy-handler/index.ts index 102783b3e1b49..f4a8b4d94bf3b 100644 --- a/packages/@aws-cdk/custom-resource-handlers/lib/aws-cloudfront-origins/s3-origin-access-control-key-policy-handler/index.ts +++ b/packages/@aws-cdk/custom-resource-handlers/lib/aws-cloudfront-origins/s3-origin-access-control-key-policy-handler/index.ts @@ -10,16 +10,15 @@ const KEY_ACTIONS: Record = { }; export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent) { + const props = event.ResourceProperties; + const distributionId = props.DistributionId; + const kmsKeyId = props.KmsKeyId; + const accountId = props.AccountId; + const partition = props.Partition; + const region = process.env.AWS_REGION; + const accessLevels = props.AccessLevels; if (event.RequestType === 'Create' || event.RequestType === 'Update') { - const props = event.ResourceProperties; - const distributionId = props.DistributionId; - const kmsKeyId = props.KmsKeyId; - const accountId = props.AccountId; - const partition = props.Partition; - const region = process.env.AWS_REGION; - const accessLevels = props.AccessLevels; - const describeKeyCommandResponse = await kms.describeKey({ KeyId: kmsKeyId, }); @@ -48,7 +47,7 @@ export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent const actions = getActions(accessLevels); const kmsKeyPolicyStatement = { - Sid: 'AllowCloudFrontServicePrincipalSSE-KMS', + Sid: 'GrantOACAccessToKMS', Effect: 'Allow', Principal: { Service: [ @@ -75,12 +74,44 @@ export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent return { IsComplete: true, }; - } else { - return; + } else if (event.RequestType === 'Delete') { + const getKeyPolicyCommandResponse = await kms.getKeyPolicy({ + KeyId: kmsKeyId, + PolicyName: 'default', + }); + + if (!getKeyPolicyCommandResponse.Policy) { + throw new Error('An error occurred while retrieving the key policy.'); + } + + const keyPolicy = JSON.parse(getKeyPolicyCommandResponse.Policy); + console.log('Retrieved key policy', JSON.stringify(keyPolicy, undefined, 2)); + + const updatedKeyPolicy = { + ...keyPolicy, + Statement: keyPolicy.Statement.filter((statement: any) => { + return ( + statement.Sid !== 'GrantOACAccessToKMS' || + statement.Condition.StringEquals['AWS:SourceArn'] !== `arn:${partition}:cloudfront::${accountId}:distribution/${distributionId}` + ); + }), + }; + + console.log('Updated key policy', JSON.stringify(updatedKeyPolicy, undefined, 2)); + + await kms.putKeyPolicy({ + KeyId: kmsKeyId, + Policy: JSON.stringify(updatedKeyPolicy), + PolicyName: 'default', + }); + + return { + IsComplete: true, + }; } } -function getActions(accessLevels: string[]): string[] { +export function getActions(accessLevels: string[]): string[] { let actions: string[] = []; for (const accessLevel of accessLevels) { actions = actions.concat(KEY_ACTIONS[accessLevel]); @@ -96,18 +127,7 @@ function getActions(accessLevels: string[]): string[] { * @param policyStatementToAdd - the policy statement to be added to the policy. * @returns currentPolicy - the updated policy. */ -function updatePolicy(currentPolicy: any, policyStatementToAdd: any) { - // // Check to see if a duplicate key policy exists by matching on the sid. This is to prevent duplicate key policies - // // from being added/updated in response to a stack being updated one or more times after initial creation. - // const existingPolicyIndex = currentPolicy.Statement.findIndex((statement: any) => statement.Sid === policyStatementToAdd.Sid); - // // If a match is found, overwrite the key policy statement... - // // Otherwise, push the new key policy to the array of statements - // if (existingPolicyIndex > -1) { - // currentPolicy.Statement[existingPolicyIndex] = policyStatementToAdd; - // } else { - // currentPolicy.Statement.push(policyStatementToAdd); - // } - +export function updatePolicy(currentPolicy: any, policyStatementToAdd: any) { if (!isStatementInPolicy(currentPolicy, policyStatementToAdd)) { currentPolicy.Statement.push(policyStatementToAdd); } @@ -115,6 +135,6 @@ function updatePolicy(currentPolicy: any, policyStatementToAdd: any) { return currentPolicy; }; -function isStatementInPolicy(policy: any, statement: any): boolean { +export function isStatementInPolicy(policy: any, statement: any): boolean { return policy.Statement.some((existingStatement: any) => JSON.stringify(existingStatement) === JSON.stringify(statement)); -} \ No newline at end of file +} \ No newline at end of file diff --git a/packages/@aws-cdk/custom-resource-handlers/test/aws-cloudfront-origins/s3-origin-access-control-bucket-policy-handler.test.ts b/packages/@aws-cdk/custom-resource-handlers/test/aws-cloudfront-origins/s3-origin-access-control-bucket-policy-handler.test.ts index cc4c6d68a0d1e..fc6d2ea1f5332 100644 --- a/packages/@aws-cdk/custom-resource-handlers/test/aws-cloudfront-origins/s3-origin-access-control-bucket-policy-handler.test.ts +++ b/packages/@aws-cdk/custom-resource-handlers/test/aws-cloudfront-origins/s3-origin-access-control-bucket-policy-handler.test.ts @@ -1,11 +1,235 @@ import { S3Client, GetBucketPolicyCommand, PutBucketPolicyCommand } from '@aws-sdk/client-s3'; -import { mockClient } from 'aws-sdk-client-mock'; +import { mockClient } from 'aws-sdk-client-mock'; import 'aws-sdk-client-mock-jest'; -// import { handler } from '../../lib/aws-cloudfront-origins/s3-origin-access-control-key-policy-handler/index'; - +import { handler, updatePolicy, isStatementInPolicy, isOaiPrincipal, removeOacPolicyStatement, removeOaiPolicyStatements, isOacPolicyStatement } from '../../lib/aws-cloudfront-origins/s3-origin-access-control-bucket-policy-handler/index'; const s3Mock = mockClient(S3Client); + +const eventCommon = { + ServiceToken: 'token', + ResponseURL: 'https://localhost', + StackId: 'stackId', + RequestId: 'requestId', + LogicalResourceId: 'logicalResourceId', + PhysicalResourceId: 'physicalId', + ResourceType: 'Custom::S3OriginAccessControlBucketPolicyUpdater', +}; + +const bucketName = 'my-bucket'; +const distributionId = 'EXAMPLE12345'; +const partition = 'aws'; +const accountId = '123456789012'; + beforeEach(() => { s3Mock.reset(); }); +describe('S3 OAC bucket policy handler', () => { + it('should call getBucketPolicy and putBucketPolicy on Create or Update event', async () => { + const event: Partial = { + ...eventCommon, + RequestType: 'Create', + ResourceProperties: { + ServiceToken: 'token', + DistributionId: distributionId, + AccountId: accountId, + Partition: partition, + BucketName: bucketName, + Actions: ['s3:GetObject'], + }, + }; + + await invokeHandler(event); + + expect(s3Mock).toHaveReceivedCommandTimes(GetBucketPolicyCommand, 1); + expect(s3Mock).toHaveReceivedCommandTimes(PutBucketPolicyCommand, 1); + }) +}); + +describe('updatePolicy', () => { + it('should add a new policy statement if it does not exist', () => { + const currentPolicy = { Statement: [] }; + const policyStatementToAdd = { Sid: 'NewStatement', Effect: 'Allow', Action: 's3:GetObject', Resource: 'arn:aws:s3:::bucket/*' }; + + const updatedPolicy = updatePolicy(currentPolicy, policyStatementToAdd); + + expect(updatedPolicy.Statement).toContainEqual(policyStatementToAdd); + }); + + it('should not add a duplicate policy statement', () => { + const currentPolicy = { Statement: [{ Sid: 'ExistingStatement', Effect: 'Allow', Action: 's3:GetObject', Resource: 'arn:aws:s3:::bucket/*' }] }; + const policyStatementToAdd = { Sid: 'ExistingStatement', Effect: 'Allow', Action: 's3:GetObject', Resource: 'arn:aws:s3:::bucket/*' }; + + const updatedPolicy = updatePolicy(currentPolicy, policyStatementToAdd); + + expect(updatedPolicy.Statement).toHaveLength(1); + }); +}); + +describe('isStatementInPolicy', () => { + it('should return true if the statement exists in the policy', () => { + const policy = { Statement: [{ Sid: 'ExistingStatement', Effect: 'Allow', Action: 's3:GetObject', Resource: 'arn:aws:s3:::bucket/*' }] }; + const statement = { Sid: 'ExistingStatement', Effect: 'Allow', Action: 's3:GetObject', Resource: 'arn:aws:s3:::bucket/*' }; + + const result = isStatementInPolicy(policy, statement); + + expect(result).toBe(true); + }); + + it('should return false if the statement does not exist in the policy', () => { + const policy = { Statement: [{ Sid: 'ExistingStatement', Effect: 'Allow', Action: 's3:GetObject', Resource: 'arn:aws:s3:::bucket/*' }] }; + const statement = { Sid: 'NewStatement', Effect: 'Allow', Action: 's3:GetObject', Resource: 'arn:aws:s3:::bucket/*' }; + + const result = isStatementInPolicy(policy, statement); + + expect(result).toBe(false); + }); +}); + +describe('removeOaiPolicyStatements', () => { + const bucketName = 'my-bucket'; + + it('should remove OAI policy statements from the bucket policy', async () => { + const bucketPolicy = { + Version: "2012-10-17", + Statement: [ + { Sid: 'Statement1', Principal: { AWS: 'cloudfront:user/CloudFront Origin Access Identity EXAMPLE12345' } }, + { Sid: 'Statement2', Principal: { AWS: 'arn:aws:iam::123456789012:root' } }, + ], + }; + + s3Mock.on(PutBucketPolicyCommand).resolves({}); + + await removeOaiPolicyStatements(bucketPolicy, bucketName); + + expect(bucketPolicy.Statement).toHaveLength(1); + expect(bucketPolicy.Statement[0].Sid).toBe('Statement2'); + expect(s3Mock).toHaveReceivedCommandTimes(PutBucketPolicyCommand, 1); + expect(s3Mock).toHaveReceivedCommandWith(PutBucketPolicyCommand, { + Bucket: bucketName, + Policy: JSON.stringify({Version: "2012-10-17", Statement: [{ Sid: 'Statement2', Principal: { AWS: 'arn:aws:iam::123456789012:root' } }] }), + }); + }); + + it('should not update the bucket policy if no OAI policy statements are found', async () => { + const bucketPolicy = { + Statement: [ + { Sid: 'Statement1', Principal: { AWS: 'arn:aws:iam::123456789012:root' } }, + { Sid: 'Statement2', Principal: { AWS: 'arn:aws:iam::123456789012:user/SomeUser' } }, + ], + }; + + await removeOaiPolicyStatements(bucketPolicy, bucketName); + + expect(bucketPolicy.Statement).toHaveLength(2); + expect(s3Mock).not.toHaveReceivedCommand(PutBucketPolicyCommand); + }); + + it('should handle an empty bucket policy', async () => { + const bucketPolicy = { Statement: [] }; + + await removeOaiPolicyStatements(bucketPolicy, bucketName); + + expect(bucketPolicy.Statement).toHaveLength(0); + expect(s3Mock).not.toHaveReceivedCommand(PutBucketPolicyCommand); + }); +}); + +describe('isOaiPrincipal', () => { + it('should return true if the statement has an OAI principal', () => { + const statement = { + Principal: { + AWS: 'cloudfront:user/CloudFront Origin Access Identity EXAMPLE12345', + }, + }; + + const result = isOaiPrincipal(statement); + + expect(result).toBe(true); + }); + + it('should return false if the statement does not have an OAI principal', () => { + const statement = { + Principal: { + AWS: 'arn:aws:iam::123456789012:root', + }, + }; + + const result = isOaiPrincipal(statement); + + expect(result).toBe(false); + }); +}); + +describe('removeOacPolicyStatement', () => { + it('should remove the OAC policy statement from the bucket policy', async () => { + const bucketPolicy = { + Statement: [ + { + Sid: 'GrantOACAccessToS3', + Principal: { Service: ['cloudfront.amazonaws.com'] }, + Effect: 'Allow', + Action: ['s3:GetObject'], + Resource: [`arn:${partition}:s3:::${bucketName}/*`], + Condition: { + StringEquals: { + 'AWS:SourceArn': `arn:${partition}:cloudfront::${accountId}:distribution/${distributionId}`, + }, + }, + }, + { Sid: 'Statement2', Principal: { AWS: 'arn:aws:iam::123456789012:root' } }, + ], + }; + + s3Mock.on(GetBucketPolicyCommand).resolves({ Policy: JSON.stringify(bucketPolicy) }); + s3Mock.on(PutBucketPolicyCommand).resolves({}); + + await removeOacPolicyStatement(bucketName, distributionId, partition, accountId); + + expect(s3Mock).toHaveReceivedCommandTimes(GetBucketPolicyCommand, 1); + expect(s3Mock).toHaveReceivedCommandTimes(PutBucketPolicyCommand, 1); + expect(s3Mock).toHaveReceivedCommandWith(PutBucketPolicyCommand, { + Bucket: bucketName, + Policy: JSON.stringify({ Statement: [{ Sid: 'Statement2', Principal: { AWS: 'arn:aws:iam::123456789012:root' } }] }), + }); + }); +}); + +describe('isOacPolicyStatement', () => { + it('should return true if the statement is an OAC policy statement', () => { + const statement = { + Sid: 'GrantOACAccessToS3', + Principal: { Service: ['cloudfront.amazonaws.com'] }, + Effect: 'Allow', + Action: ['s3:GetObject'], + Resource: ['arn:aws:s3:::bucket/*'], + Condition: { + StringEquals: { + 'AWS:SourceArn': 'arn:aws:cloudfront::123456789012:distribution/EXAMPLE12345', + }, + }, + }; + + const result = isOacPolicyStatement(statement, distributionId, partition, accountId); + + expect(result).toBe(true); + }); + + it('should return false if the statement is not an OAC policy statement', () => { + const statement = { + Sid: 'Statement2', + Principal: { AWS: 'arn:aws:iam::123456789012:root' }, + }; + + const result = isOacPolicyStatement(statement, distributionId, partition, accountId); + + expect(result).toBe(false); + }); +}); + + +// Helper function to get around TypeScript expecting a complete event object, +// even though our tests only need some of the fields +async function invokeHandler(event: Partial) { + return handler(event as AWSLambda.CloudFormationCustomResourceEvent); +} \ No newline at end of file diff --git a/packages/@aws-cdk/custom-resource-handlers/test/aws-cloudfront-origins/s3-origin-access-control-key-policy-handler.test.ts b/packages/@aws-cdk/custom-resource-handlers/test/aws-cloudfront-origins/s3-origin-access-control-key-policy-handler.test.ts index e69de29bb2d1d..1ecf41ddd8ce0 100644 --- a/packages/@aws-cdk/custom-resource-handlers/test/aws-cloudfront-origins/s3-origin-access-control-key-policy-handler.test.ts +++ b/packages/@aws-cdk/custom-resource-handlers/test/aws-cloudfront-origins/s3-origin-access-control-key-policy-handler.test.ts @@ -0,0 +1,196 @@ +import { KMS, KeyManagerType, DescribeKeyCommand, PutKeyPolicyCommand, GetKeyPolicyCommand } from '@aws-sdk/client-kms'; +import { mockClient } from 'aws-sdk-client-mock'; +import 'aws-sdk-client-mock-jest'; +import { handler, updatePolicy, getActions, isStatementInPolicy } from '../../lib/aws-cloudfront-origins/s3-origin-access-control-key-policy-handler/index'; + +const kmsMock = mockClient(KMS); + +beforeEach(() => { + kmsMock.reset(); +}); + +const keyId = '1a2b3c4de-123a-4bcd-ef5g-ab123abc12bcd23' +const distributionId = 'EXAMPLE12345'; +const partition = 'aws'; +const accountId = '123456789012'; + +describe('S3 OAC key policy handler', () => { + it('should skip updating the key policy for AWS managed keys', async () => { + const event = { + RequestType: 'Create', + ResourceProperties: { + DistributionId: distributionId, + KmsKeyId: keyId, + AccountId: accountId, + Partition: partition, + AccessLevels: ['READ'], + }, + }; + + kmsMock.on(DescribeKeyCommand).resolves({ + KeyMetadata: { + KeyId: keyId, + KeyManager: KeyManagerType.AWS, + }, + }); + + const response = await handler(event as any); + expect(response).toBeUndefined(); + expect(kmsMock).not.toHaveReceivedCommand(GetKeyPolicyCommand); + expect(kmsMock).not.toHaveReceivedCommand(PutKeyPolicyCommand); + }); + + it('should update the key policy for customer managed keys on Create/Update', async () => { + const event = { + RequestType: 'Create', + ResourceProperties: { + DistributionId: distributionId, + KmsKeyId: keyId, + AccountId: accountId, + Partition: partition, + AccessLevels: ['READ', 'WRITE'], + }, + }; + + const keyPolicy = { + Statement: [], + }; + + kmsMock.on(DescribeKeyCommand).resolves({ + KeyMetadata: { + KeyId: keyId, + KeyManager: KeyManagerType.CUSTOMER, + }, + }); + kmsMock.on(GetKeyPolicyCommand).resolves({ Policy: JSON.stringify(keyPolicy) }); + kmsMock.on(PutKeyPolicyCommand).resolves({}); + + const response = await handler(event as any); + expect(response).toEqual({ IsComplete: true }); + expect(kmsMock).toHaveReceivedCommandTimes(GetKeyPolicyCommand, 1); + expect(kmsMock).toHaveReceivedCommandTimes(PutKeyPolicyCommand, 1); + }); + + it('should remove the OAC policy statement on Delete', async () => { + const event = { + RequestType: 'Delete', + ResourceProperties: { + DistributionId: distributionId, + KmsKeyId: keyId, + AccountId: accountId, + Partition: partition, + }, + }; + + const keyPolicy = { + Statement: [ + { + Sid: 'GrantOACAccessToKMS', + Effect: 'Allow', + Principal: { + Service: ['cloudfront.amazonaws.com'], + }, + Action: ['kms:Decrypt', 'kms:Encrypt', 'kms:GenerateDataKey*'], + Resource: 'arn:aws:kms:us-east-1:123456789012:key/my-key', + Condition: { + StringEquals: { + 'AWS:SourceArn': 'arn:aws:cloudfront::123456789012:distribution/EXAMPLE12345', + }, + }, + }, + { + Sid: 'AnotherStatement', + Effect: 'Allow', + Principal: { + AWS: 'arn:aws:iam::123456789012:root', + }, + Action: ['kms:*'], + Resource: '*', + }, + ], + }; + + kmsMock.on(GetKeyPolicyCommand).resolves({ Policy: JSON.stringify(keyPolicy) }); + kmsMock.on(PutKeyPolicyCommand).resolves({}); + + const response = await handler(event as any); + expect(response).toEqual({ IsComplete: true }); + expect(kmsMock).toHaveReceivedCommandTimes(GetKeyPolicyCommand, 1); + expect(kmsMock).toHaveReceivedCommandTimes(PutKeyPolicyCommand, 1); + expect(kmsMock).toHaveReceivedCommandWith(PutKeyPolicyCommand, { + KeyId: keyId, + Policy: JSON.stringify({ + Statement: [ + { + Sid: 'AnotherStatement', + Effect: 'Allow', + Principal: { + AWS: 'arn:aws:iam::123456789012:root', + }, + Action: ['kms:*'], + Resource: '*', + }, + ], + }), + PolicyName: 'default', + }); + }); +}); + +describe('getActions', () => { + it('should return the correct actions for the given access levels', () => { + expect(getActions(['READ'])).toEqual(['kms:Decrypt']); + expect(getActions(['WRITE'])).toEqual(['kms:Encrypt', 'kms:GenerateDataKey*']); + expect(getActions(['READ', 'WRITE'])).toEqual(['kms:Decrypt', 'kms:Encrypt', 'kms:GenerateDataKey*']); + }); + + it('should return an empty array for an empty access levels array', () => { + expect(getActions([])).toEqual([]); + }); +}); + +describe('updatePolicy', () => { + it('should add a new policy statement if it does not exist', () => { + const currentPolicy = { Statement: [] }; + const policyStatementToAdd = { Sid: 'NewStatement', Effect: 'Allow', Action: ['kms:Decrypt'], Resource: '*' }; + const updatedPolicy = updatePolicy(currentPolicy, policyStatementToAdd); + expect(updatedPolicy.Statement).toHaveLength(1); + expect(updatedPolicy.Statement[0]).toEqual(policyStatementToAdd); + }); + + it('should not add a duplicate policy statement', () => { + const currentPolicy = { + Statement: [ + { Sid: 'ExistingStatement', Effect: 'Allow', Action: ['kms:Decrypt'], Resource: '*' }, + ], + }; + const policyStatementToAdd = { Sid: 'ExistingStatement', Effect: 'Allow', Action: ['kms:Decrypt'], Resource: '*' }; + const updatedPolicy = updatePolicy(currentPolicy, policyStatementToAdd); + expect(updatedPolicy.Statement).toHaveLength(1); + expect(updatedPolicy.Statement[0]).toEqual(policyStatementToAdd); + }); +}); + +describe('isStatementInPolicy', () => { + it('should return true if the statement exists in the policy', () => { + const policy = { + Statement: [ + { Sid: 'Statement1', Effect: 'Allow', Action: ['kms:Decrypt'], Resource: '*' }, + { Sid: 'Statement2', Effect: 'Deny', Action: ['kms:Encrypt'], Resource: '*' }, + ], + }; + const statement = { Sid: 'Statement1', Effect: 'Allow', Action: ['kms:Decrypt'], Resource: '*' }; + expect(isStatementInPolicy(policy, statement)).toBe(true); + }); + + it('should return false if the statement does not exist in the policy', () => { + const policy = { + Statement: [ + { Sid: 'Statement1', Effect: 'Allow', Action: ['kms:Decrypt'], Resource: '*' }, + { Sid: 'Statement2', Effect: 'Deny', Action: ['kms:Encrypt'], Resource: '*' }, + ], + }; + const statement = { Sid: 'Statement3', Effect: 'Allow', Action: ['kms:GenerateDataKey'], Resource: '*' }; + expect(isStatementInPolicy(policy, statement)).toBe(false); + }); +}); \ No newline at end of file diff --git a/packages/aws-cdk-lib/aws-cloudfront-origins/lib/s3-origin.ts b/packages/aws-cdk-lib/aws-cloudfront-origins/lib/s3-origin.ts index bb7b9e95f8d01..c0b07281c8771 100644 --- a/packages/aws-cdk-lib/aws-cloudfront-origins/lib/s3-origin.ts +++ b/packages/aws-cdk-lib/aws-cloudfront-origins/lib/s3-origin.ts @@ -44,7 +44,7 @@ export interface S3OriginProps extends cloudfront.OriginProps { /** * The level of permissions granted in the bucket policy and key policy (if applicable) - * to the CloudFront distribution. + * to the CloudFront distribution. This property only applies to OAC (not OAI). * @default AccessLevel.READ */ readonly originAccessLevels?: AccessLevel[]; diff --git a/packages/aws-cdk-lib/aws-cloudfront-origins/test/s3-origin.test.ts b/packages/aws-cdk-lib/aws-cloudfront-origins/test/s3-origin.test.ts index 68272dd8b95ab..f021b3a43ecca 100644 --- a/packages/aws-cdk-lib/aws-cloudfront-origins/test/s3-origin.test.ts +++ b/packages/aws-cdk-lib/aws-cloudfront-origins/test/s3-origin.test.ts @@ -1,8 +1,10 @@ -import { Match, Template } from '../../assertions'; +import { Annotations, Match, Template } from '../../assertions'; import * as cloudfront from '../../aws-cloudfront'; import * as s3 from '../../aws-s3'; +import * as kms from '../../aws-kms'; import { App, Duration, Stack } from '../../core'; -import { S3Origin } from '../lib'; +import { CLOUDFRONT_USE_ORIGIN_ACCESS_CONTROL_BY_DEFAULT } from '../../cx-api'; +import { AccessLevel, S3Origin, S3OriginProps } from '../lib'; let app: App; let stack: Stack; @@ -191,6 +193,7 @@ describe('With bucket', () => { }, }); }); + test('Cannot set an originId duplicates', () => { const bucket = new s3.Bucket(stack, 'Bucket'); const bucket2 = new s3.Bucket(stack, 'Bucket2'); @@ -207,8 +210,164 @@ describe('With bucket', () => { }); }).toThrow(/Origin with id MyCustomOrigin already exists/); }); + + test('Cannot specify both OAI and OAC', () => { + const bucket = new s3.Bucket(stack, 'Bucket'); + const oai = new cloudfront.OriginAccessIdentity(stack, 'OAI'); + const oac = new cloudfront.OriginAccessControl(stack, 'OAC'); + expect(() => + new S3Origin(bucket, { + originAccessIdentity: oai, + originAccessControl: oac, + })).toThrow('Only one of originAccessControl or originAccessIdentity can be specified for an origin.'); + }) + + describe('with feature flag enabled', () => { + beforeEach(() => { + stack.node.setContext(CLOUDFRONT_USE_ORIGIN_ACCESS_CONTROL_BY_DEFAULT, true); + }); + test('should create an S3Origin with Origin Access Control by default', () => { + const bucket = new s3.Bucket(stack, 'Bucket'); + const origin = new S3Origin(bucket); + const bindConfig = origin.bind(stack, { originId: 'TestOrigin' }); + expect(bindConfig.originProperty?.originAccessControlId).toBeDefined(); + Template.fromStack(stack).resourceCountIs('AWS::CloudFront::OriginAccessControl', 1); + }); + + test('should create an S3Origin with OAI when specified by user', () => { + const props: S3OriginProps = { + originAccessIdentity: new cloudfront.OriginAccessIdentity(stack, 'TestOAI'), + }; + const bucket = new s3.Bucket(stack, 'Bucket'); + const origin = new S3Origin(bucket, props); + const bindConfig = origin.bind(stack, { originId: 'TestOrigin' }); + expect(bindConfig.originProperty?.originAccessControlId).toBeUndefined(); + Template.fromStack(stack).hasResourceProperties('AWS::CloudFront::CloudFrontOriginAccessIdentity', { + CloudFrontOriginAccessIdentityConfig: { + Comment: 'Allows CloudFront to reach the bucket', + }, + }); + Template.fromStack(stack).resourceCountIs('AWS::CloudFront::CloudFrontOriginAccessIdentity', 1); + Template.fromStack(stack).resourceCountIs('AWS::CloudFront::OriginAccessControl', 0); + }); + + test('should create an S3Origin with Origin Access Control when specified', () => { + const bucket = new s3.Bucket(stack, 'Bucket'); + const props: S3OriginProps = { + originAccessControl: new cloudfront.OriginAccessControl(stack, 'TestOAC'), + }; + const origin = new S3Origin(bucket, props); + const bindConfig = origin.bind(stack, { originId: 'TestOrigin' }); + expect(bindConfig.originProperty?.originAccessControlId).toBeDefined(); + Template.fromStack(stack).resourceCountIs('AWS::CloudFront::OriginAccessControl', 1); + Template.fromStack(stack).resourceCountIs('AWS::CloudFront::CloudFrontOriginAccessIdentity', 0); + }); + + test('should create an S3Origin with the specified access levels', () => { + const bucket = new s3.Bucket(stack, 'Bucket'); + const props: S3OriginProps = { + originAccessLevels: [AccessLevel.READ, AccessLevel.WRITE], + }; + const origin = new S3Origin(bucket, props); + new cloudfront.Distribution(stack, 'Dist', { defaultBehavior: { origin } }); + console.log(Template.fromStack(stack).findResources('AWS::S3::BucketPolicy')); + Template.fromStack(stack).hasResourceProperties('AWS::S3::BucketPolicy', { + PolicyDocument: { + Statement: [{ + Sid: "GrantOACAccessToS3", + Action: ['s3:GetObject', 's3:PutObject'], + Effect: 'Allow', + Principal: { + Service: "cloudfront.amazonaws.com", + }, + Resource: { + 'Fn::Join': ['', [{ 'Fn::GetAtt': ['Bucket83908E77', 'Arn'] }, '/*']], + }, + Condition: { + StringEquals: { + 'AWS:SourceArn': { + 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':cloudfront::', { Ref: 'AWS::AccountId' }, ':distribution/', { Ref: 'DistB3B78991'} ]], + }, + }, + }, + }], + }, + }); + }); + + test('should throw warning when trying to update imported bucket policy without overrideImportedBucketPolicy set', () => { + const importedBucket = s3.Bucket.fromBucketName(stack, 'myImportedBucket', 'bucket-name'); + const props: S3OriginProps = { + originAccessControl: new cloudfront.OriginAccessControl(stack, 'TestOAC'), + }; + const origin = new S3Origin(importedBucket, props); + origin.bind(stack, { originId: 'TestOrigin' }); + Annotations.fromStack(stack).hasWarning('*', 'Cannot update bucket policy of an imported bucket. Set overrideImportedBucketPolicy to true or update the policy manually instead. [ack: @aws-cdk/aws-cloudfront-origins:updateBucketPolicy]'); + }); + + test('should create custom resource to update imported bucket policy when overrideImportedBucketPolicy is set', () => { + const importedBucket = s3.Bucket.fromBucketName(stack, 'myImportedBucket', 'bucket-name'); + const props: S3OriginProps = { + originAccessControl: new cloudfront.OriginAccessControl(stack, 'TestOAC'), + overrideImportedBucketPolicy: true, + }; + const origin = new S3Origin(importedBucket, props); + origin.bind(stack, { originId: 'TestOrigin' }); + Template.fromStack(stack).resourceCountIs('Custom::S3OriginAccessControlBucketPolicyUpdater', 1); + }); + + test('should create custom resource to update key policy for SSE-KMS buckets', () => { + const bucket = new s3.Bucket(stack, 'myBucket', { + encryption: s3.BucketEncryption.KMS, + encryptionKey: new kms.Key(stack, 'Key'), + }); + const props: S3OriginProps = { + originAccessControl: new cloudfront.OriginAccessControl(stack, 'TestOAC'), + overrideImportedBucketPolicy: true, + }; + const origin = new S3Origin(bucket, props); + origin.bind(stack, { originId: 'TestOrigin' }); + Template.fromStack(stack).resourceCountIs('Custom::S3OriginAccessControlKeyPolicyUpdater', 1); + }); + }); + + describe('with feature flag disabled (default)', () => { + test('should create an S3Origin with Origin Access Identity by default', () => { + const bucket = new s3.Bucket(stack, 'Bucket'); + const origin = new S3Origin(bucket); + const bindConfig = origin.bind(stack, { originId: 'TestOrigin' }); + expect(bindConfig.originProperty?.originAccessControlId).toBeUndefined(); + Template.fromStack(stack).resourceCountIs('AWS::CloudFront::CloudFrontOriginAccessIdentity', 1); + Template.fromStack(stack).resourceCountIs('AWS::CloudFront::OriginAccessControl', 0); + }); + + test('should create an S3Origin with Origin Access Identity when specified', () => { + const bucket = new s3.Bucket(stack, 'Bucket'); + const props: S3OriginProps = { + originAccessIdentity: new cloudfront.OriginAccessIdentity(stack, 'TestOAI'), + }; + const origin = new S3Origin(bucket, props); + const bindConfig = origin.bind(stack, { originId: 'TestOrigin' }); + expect(bindConfig.originProperty?.originAccessControlId).toBeUndefined(); + Template.fromStack(stack).resourceCountIs('AWS::CloudFront::CloudFrontOriginAccessIdentity', 1); + Template.fromStack(stack).resourceCountIs('AWS::CloudFront::OriginAccessControl', 0); + }); + + test('should create an S3Origin with Origin Access Control when specified', () => { + const bucket = new s3.Bucket(stack, 'Bucket'); + const props: S3OriginProps = { + originAccessControl: new cloudfront.OriginAccessControl(stack, 'TestOAC'), + }; + const origin = new S3Origin(bucket, props); + const bindConfig = origin.bind(stack, { originId: 'TestOrigin' }); + expect(bindConfig.originProperty?.originAccessControlId).toBeDefined(); + Template.fromStack(stack).resourceCountIs('AWS::CloudFront::CloudFrontOriginAccessIdentity', 0); + Template.fromStack(stack).resourceCountIs('AWS::CloudFront::OriginAccessControl', 1); + }); + }); }); + describe('With website-configured bucket', () => { test('renders all required properties, including custom origin config', () => { const bucket = new s3.Bucket(stack, 'Bucket', { From d1f4868605ff60fb3492d8128ed414c76856cc24 Mon Sep 17 00:00:00 2001 From: gracelu0 Date: Tue, 25 Jun 2024 11:17:58 -0700 Subject: [PATCH 12/14] Add integration tests and update README --- .../__entrypoint__.js | 155 +++++++ .../index.js | 1 + .../cdk.out | 1 + .../integ.json | 12 + ...efaultTestDeployAssert38960359.assets.json | 19 + ...aultTestDeployAssert38960359.template.json | 36 ++ .../manifest.json | 161 +++++++ .../ssekms-s3-origin-oac.assets.json | 32 ++ .../ssekms-s3-origin-oac.template.json | 418 +++++++++++++++++ .../tree.json | 420 ++++++++++++++++++ .../test/integ.s3-origin-oac-ssekms.ts | 28 ++ .../integ.s3-origin-oac.js.snapshot/cdk.out | 1 + .../cloudfront-s3-origin-oac.assets.json | 19 + .../cloudfront-s3-origin-oac.template.json | 148 ++++++ .../integ.json | 12 + .../manifest.json | 131 ++++++ ...efaultTestDeployAssertD8FD270A.assets.json | 19 + ...aultTestDeployAssertD8FD270A.template.json | 36 ++ .../integ.s3-origin-oac.js.snapshot/tree.json | 291 ++++++++++++ .../test/integ.s3-origin-oac.ts | 23 + .../index.ts | 6 +- .../index.ts | 12 +- ...cess-control-bucket-policy-handler.test.ts | 4 +- ...-access-control-key-policy-handler.test.ts | 6 +- .../aws-cloudfront-origins/README.md | 103 ++++- .../aws-cloudfront-origins/lib/s3-origin.ts | 9 +- .../test/s3-origin.test.ts | 12 +- packages/aws-cdk-lib/aws-cloudfront/README.md | 105 ++++- .../aws-cdk-lib/aws-cloudfront/lib/origin.ts | 1 + .../aws-cloudfront/lib/web-distribution.ts | 1 - packages/aws-cdk-lib/cx-api/FEATURE_FLAGS.md | 10 +- 31 files changed, 2198 insertions(+), 34 deletions(-) create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront-origins/test/integ.s3-origin-oac-ssekms.js.snapshot/asset.b1d5dd3578b58b3875b24727941d7b7ddc396de56dd9734b76c56a93c115c449/__entrypoint__.js create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront-origins/test/integ.s3-origin-oac-ssekms.js.snapshot/asset.b1d5dd3578b58b3875b24727941d7b7ddc396de56dd9734b76c56a93c115c449/index.js create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront-origins/test/integ.s3-origin-oac-ssekms.js.snapshot/cdk.out create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront-origins/test/integ.s3-origin-oac-ssekms.js.snapshot/integ.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront-origins/test/integ.s3-origin-oac-ssekms.js.snapshot/kmss3originoacDefaultTestDeployAssert38960359.assets.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront-origins/test/integ.s3-origin-oac-ssekms.js.snapshot/kmss3originoacDefaultTestDeployAssert38960359.template.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront-origins/test/integ.s3-origin-oac-ssekms.js.snapshot/manifest.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront-origins/test/integ.s3-origin-oac-ssekms.js.snapshot/ssekms-s3-origin-oac.assets.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront-origins/test/integ.s3-origin-oac-ssekms.js.snapshot/ssekms-s3-origin-oac.template.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront-origins/test/integ.s3-origin-oac-ssekms.js.snapshot/tree.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront-origins/test/integ.s3-origin-oac-ssekms.ts create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront-origins/test/integ.s3-origin-oac.js.snapshot/cdk.out create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront-origins/test/integ.s3-origin-oac.js.snapshot/cloudfront-s3-origin-oac.assets.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront-origins/test/integ.s3-origin-oac.js.snapshot/cloudfront-s3-origin-oac.template.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront-origins/test/integ.s3-origin-oac.js.snapshot/integ.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront-origins/test/integ.s3-origin-oac.js.snapshot/manifest.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront-origins/test/integ.s3-origin-oac.js.snapshot/s3originoacDefaultTestDeployAssertD8FD270A.assets.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront-origins/test/integ.s3-origin-oac.js.snapshot/s3originoacDefaultTestDeployAssertD8FD270A.template.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront-origins/test/integ.s3-origin-oac.js.snapshot/tree.json diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront-origins/test/integ.s3-origin-oac-ssekms.js.snapshot/asset.b1d5dd3578b58b3875b24727941d7b7ddc396de56dd9734b76c56a93c115c449/__entrypoint__.js b/packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront-origins/test/integ.s3-origin-oac-ssekms.js.snapshot/asset.b1d5dd3578b58b3875b24727941d7b7ddc396de56dd9734b76c56a93c115c449/__entrypoint__.js new file mode 100644 index 0000000000000..02033f55cf612 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront-origins/test/integ.s3-origin-oac-ssekms.js.snapshot/asset.b1d5dd3578b58b3875b24727941d7b7ddc396de56dd9734b76c56a93c115c449/__entrypoint__.js @@ -0,0 +1,155 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.withRetries = exports.handler = exports.external = void 0; +const https = require("https"); +const url = require("url"); +// for unit tests +exports.external = { + sendHttpRequest: defaultSendHttpRequest, + log: defaultLog, + includeStackTraces: true, + userHandlerIndex: './index', +}; +const CREATE_FAILED_PHYSICAL_ID_MARKER = 'AWSCDK::CustomResourceProviderFramework::CREATE_FAILED'; +const MISSING_PHYSICAL_ID_MARKER = 'AWSCDK::CustomResourceProviderFramework::MISSING_PHYSICAL_ID'; +async function handler(event, context) { + const sanitizedEvent = { ...event, ResponseURL: '...' }; + exports.external.log(JSON.stringify(sanitizedEvent, undefined, 2)); + // ignore DELETE event when the physical resource ID is the marker that + // indicates that this DELETE is a subsequent DELETE to a failed CREATE + // operation. + if (event.RequestType === 'Delete' && event.PhysicalResourceId === CREATE_FAILED_PHYSICAL_ID_MARKER) { + exports.external.log('ignoring DELETE event caused by a failed CREATE event'); + await submitResponse('SUCCESS', event); + return; + } + try { + // invoke the user handler. this is intentionally inside the try-catch to + // ensure that if there is an error it's reported as a failure to + // cloudformation (otherwise cfn waits). + // eslint-disable-next-line @typescript-eslint/no-require-imports + const userHandler = require(exports.external.userHandlerIndex).handler; + const result = await userHandler(sanitizedEvent, context); + // validate user response and create the combined event + const responseEvent = renderResponse(event, result); + // submit to cfn as success + await submitResponse('SUCCESS', responseEvent); + } + catch (e) { + const resp = { + ...event, + Reason: exports.external.includeStackTraces ? e.stack : e.message, + }; + if (!resp.PhysicalResourceId) { + // special case: if CREATE fails, which usually implies, we usually don't + // have a physical resource id. in this case, the subsequent DELETE + // operation does not have any meaning, and will likely fail as well. to + // address this, we use a marker so the provider framework can simply + // ignore the subsequent DELETE. + if (event.RequestType === 'Create') { + exports.external.log('CREATE failed, responding with a marker physical resource id so that the subsequent DELETE will be ignored'); + resp.PhysicalResourceId = CREATE_FAILED_PHYSICAL_ID_MARKER; + } + else { + // otherwise, if PhysicalResourceId is not specified, something is + // terribly wrong because all other events should have an ID. + exports.external.log(`ERROR: Malformed event. "PhysicalResourceId" is required: ${JSON.stringify(event)}`); + } + } + // this is an actual error, fail the activity altogether and exist. + await submitResponse('FAILED', resp); + } +} +exports.handler = handler; +function renderResponse(cfnRequest, handlerResponse = {}) { + // if physical ID is not returned, we have some defaults for you based + // on the request type. + const physicalResourceId = handlerResponse.PhysicalResourceId ?? cfnRequest.PhysicalResourceId ?? cfnRequest.RequestId; + // if we are in DELETE and physical ID was changed, it's an error. + if (cfnRequest.RequestType === 'Delete' && physicalResourceId !== cfnRequest.PhysicalResourceId) { + throw new Error(`DELETE: cannot change the physical resource ID from "${cfnRequest.PhysicalResourceId}" to "${handlerResponse.PhysicalResourceId}" during deletion`); + } + // merge request event and result event (result prevails). + return { + ...cfnRequest, + ...handlerResponse, + PhysicalResourceId: physicalResourceId, + }; +} +async function submitResponse(status, event) { + const json = { + Status: status, + Reason: event.Reason ?? status, + StackId: event.StackId, + RequestId: event.RequestId, + PhysicalResourceId: event.PhysicalResourceId || MISSING_PHYSICAL_ID_MARKER, + LogicalResourceId: event.LogicalResourceId, + NoEcho: event.NoEcho, + Data: event.Data, + }; + const parsedUrl = url.parse(event.ResponseURL); + const loggingSafeUrl = `${parsedUrl.protocol}//${parsedUrl.hostname}/${parsedUrl.pathname}?***`; + exports.external.log('submit response to cloudformation', loggingSafeUrl, json); + const responseBody = JSON.stringify(json); + const req = { + hostname: parsedUrl.hostname, + path: parsedUrl.path, + method: 'PUT', + headers: { + 'content-type': '', + 'content-length': Buffer.byteLength(responseBody, 'utf8'), + }, + }; + const retryOptions = { + attempts: 5, + sleep: 1000, + }; + await withRetries(retryOptions, exports.external.sendHttpRequest)(req, responseBody); +} +async function defaultSendHttpRequest(options, requestBody) { + return new Promise((resolve, reject) => { + try { + const request = https.request(options, (response) => { + response.resume(); // Consume the response but don't care about it + if (!response.statusCode || response.statusCode >= 400) { + reject(new Error(`Unsuccessful HTTP response: ${response.statusCode}`)); + } + else { + resolve(); + } + }); + request.on('error', reject); + request.write(requestBody); + request.end(); + } + catch (e) { + reject(e); + } + }); +} +function defaultLog(fmt, ...params) { + // eslint-disable-next-line no-console + console.log(fmt, ...params); +} +function withRetries(options, fn) { + return async (...xs) => { + let attempts = options.attempts; + let ms = options.sleep; + while (true) { + try { + return await fn(...xs); + } + catch (e) { + if (attempts-- <= 0) { + throw e; + } + await sleep(Math.floor(Math.random() * ms)); + ms *= 2; + } + } + }; +} +exports.withRetries = withRetries; +async function sleep(ms) { + return new Promise((ok) => setTimeout(ok, ms)); +} diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront-origins/test/integ.s3-origin-oac-ssekms.js.snapshot/asset.b1d5dd3578b58b3875b24727941d7b7ddc396de56dd9734b76c56a93c115c449/index.js b/packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront-origins/test/integ.s3-origin-oac-ssekms.js.snapshot/asset.b1d5dd3578b58b3875b24727941d7b7ddc396de56dd9734b76c56a93c115c449/index.js new file mode 100644 index 0000000000000..43dfffd5b7f48 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront-origins/test/integ.s3-origin-oac-ssekms.js.snapshot/asset.b1d5dd3578b58b3875b24727941d7b7ddc396de56dd9734b76c56a93c115c449/index.js @@ -0,0 +1 @@ +"use strict";var r=Object.defineProperty;var S=Object.getOwnPropertyDescriptor;var K=Object.getOwnPropertyNames;var g=Object.prototype.hasOwnProperty;var P=(t,e)=>{for(var n in e)r(t,n,{get:e[n],enumerable:!0})},I=(t,e,n,i)=>{if(e&&typeof e=="object"||typeof e=="function")for(let o of K(e))!g.call(t,o)&&o!==n&&r(t,o,{get:()=>e[o],enumerable:!(i=S(e,o))||i.enumerable});return t};var A=t=>I(r({},"__esModule",{value:!0}),t);var N={};P(N,{handler:()=>k});module.exports=A(N);var s=require("@aws-sdk/client-kms"),c=new s.KMS({}),R={READ:["kms:Decrypt"],WRITE:["kms:Encrypt","kms:GenerateDataKey*"]};async function k(t){if(t.RequestType==="Create"||t.RequestType==="Update"){let e=t.ResourceProperties,n=e.DistributionId,i=e.KmsKeyId,o=e.AccountId,a=e.Partition,u=process.env.AWS_REGION,m=e.AccessLevels;if((await c.describeKey({KeyId:i})).KeyMetadata?.KeyManager===s.KeyManagerType.AWS)return;let y=await c.getKeyPolicy({KeyId:i,PolicyName:"default"});if(!y.Policy)throw new Error("An error occurred while retrieving the key policy.");let d=JSON.parse(y?.Policy);console.log("Retrieved key policy",JSON.stringify(d,void 0,2));let p=w(m),f={Sid:"AllowCloudFrontServicePrincipalSSE-KMS",Effect:"Allow",Principal:{Service:["cloudfront.amazonaws.com"]},Action:p,Resource:`arn:${a}:kms:${u}:${o}:key/${i}`,Condition:{StringEquals:{"AWS:SourceArn":`arn:${a}:cloudfront::${o}:distribution/${n}`}}},l=C(d,f);return console.log("Updated key policy",JSON.stringify(l,void 0,2)),await c.putKeyPolicy({KeyId:i,Policy:JSON.stringify(l),PolicyName:"default"}),{IsComplete:!0}}else return}function w(t){let e=[];for(let n of t)e=e.concat(R[n]);return e}function C(t,e){return E(t,e)||t.Statement.push(e),t}function E(t,e){return t.Statement.some(n=>JSON.stringify(n)===JSON.stringify(e))}0&&(module.exports={handler}); diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront-origins/test/integ.s3-origin-oac-ssekms.js.snapshot/cdk.out b/packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront-origins/test/integ.s3-origin-oac-ssekms.js.snapshot/cdk.out new file mode 100644 index 0000000000000..1f0068d32659a --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront-origins/test/integ.s3-origin-oac-ssekms.js.snapshot/cdk.out @@ -0,0 +1 @@ +{"version":"36.0.0"} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront-origins/test/integ.s3-origin-oac-ssekms.js.snapshot/integ.json b/packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront-origins/test/integ.s3-origin-oac-ssekms.js.snapshot/integ.json new file mode 100644 index 0000000000000..3c1e51bcd33fc --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront-origins/test/integ.s3-origin-oac-ssekms.js.snapshot/integ.json @@ -0,0 +1,12 @@ +{ + "version": "36.0.0", + "testCases": { + "kms-s3-origin-oac/DefaultTest": { + "stacks": [ + "ssekms-s3-origin-oac" + ], + "assertionStack": "kms-s3-origin-oac/DefaultTest/DeployAssert", + "assertionStackName": "kmss3originoacDefaultTestDeployAssert38960359" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront-origins/test/integ.s3-origin-oac-ssekms.js.snapshot/kmss3originoacDefaultTestDeployAssert38960359.assets.json b/packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront-origins/test/integ.s3-origin-oac-ssekms.js.snapshot/kmss3originoacDefaultTestDeployAssert38960359.assets.json new file mode 100644 index 0000000000000..2faf0d7c4195f --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront-origins/test/integ.s3-origin-oac-ssekms.js.snapshot/kmss3originoacDefaultTestDeployAssert38960359.assets.json @@ -0,0 +1,19 @@ +{ + "version": "36.0.0", + "files": { + "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22": { + "source": { + "path": "kmss3originoacDefaultTestDeployAssert38960359.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22.json", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + } + }, + "dockerImages": {} +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront-origins/test/integ.s3-origin-oac-ssekms.js.snapshot/kmss3originoacDefaultTestDeployAssert38960359.template.json b/packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront-origins/test/integ.s3-origin-oac-ssekms.js.snapshot/kmss3originoacDefaultTestDeployAssert38960359.template.json new file mode 100644 index 0000000000000..ad9d0fb73d1dd --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront-origins/test/integ.s3-origin-oac-ssekms.js.snapshot/kmss3originoacDefaultTestDeployAssert38960359.template.json @@ -0,0 +1,36 @@ +{ + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront-origins/test/integ.s3-origin-oac-ssekms.js.snapshot/manifest.json b/packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront-origins/test/integ.s3-origin-oac-ssekms.js.snapshot/manifest.json new file mode 100644 index 0000000000000..a92919a25519b --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront-origins/test/integ.s3-origin-oac-ssekms.js.snapshot/manifest.json @@ -0,0 +1,161 @@ +{ + "version": "36.0.0", + "artifacts": { + "ssekms-s3-origin-oac.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "ssekms-s3-origin-oac.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "ssekms-s3-origin-oac": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "ssekms-s3-origin-oac.template.json", + "terminationProtection": false, + "validateOnSynth": false, + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", + "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/8e090bce96f44f628ab8baaa9bd7db50201c5df8945bf80d5737fbd954753e59.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", + "additionalDependencies": [ + "ssekms-s3-origin-oac.assets" + ], + "lookupRole": { + "arn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}", + "requiresBootstrapStackVersion": 8, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "dependencies": [ + "ssekms-s3-origin-oac.assets" + ], + "metadata": { + "/ssekms-s3-origin-oac/Key/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "Key961B73FD" + } + ], + "/ssekms-s3-origin-oac/Bucket/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "Bucket83908E77" + } + ], + "/ssekms-s3-origin-oac/Bucket/Policy/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "BucketPolicyE9A3008A" + } + ], + "/ssekms-s3-origin-oac/OriginAccessControl/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "OriginAccessControlDC2011A2" + } + ], + "/ssekms-s3-origin-oac/Distribution/Origin1/S3OriginKMSKeyPolicyCustomResource/Default": [ + { + "type": "aws:cdk:logicalId", + "data": "DistributionOrigin1S3OriginKMSKeyPolicyCustomResource29A87268" + } + ], + "/ssekms-s3-origin-oac/Distribution/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "Distribution830FAC52" + } + ], + "/ssekms-s3-origin-oac/LatestNodeRuntimeMap": [ + { + "type": "aws:cdk:logicalId", + "data": "LatestNodeRuntimeMap" + } + ], + "/ssekms-s3-origin-oac/Custom::S3OriginAccessControlKeyPolicyUpdaterCustomResourceProvider/Role": [ + { + "type": "aws:cdk:logicalId", + "data": "CustomS3OriginAccessControlKeyPolicyUpdaterCustomResourceProviderRole721641A7" + } + ], + "/ssekms-s3-origin-oac/Custom::S3OriginAccessControlKeyPolicyUpdaterCustomResourceProvider/Handler": [ + { + "type": "aws:cdk:logicalId", + "data": "CustomS3OriginAccessControlKeyPolicyUpdaterCustomResourceProviderHandler6A458930" + } + ], + "/ssekms-s3-origin-oac/BootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "BootstrapVersion" + } + ], + "/ssekms-s3-origin-oac/CheckBootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "CheckBootstrapVersion" + } + ] + }, + "displayName": "ssekms-s3-origin-oac" + }, + "kmss3originoacDefaultTestDeployAssert38960359.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "kmss3originoacDefaultTestDeployAssert38960359.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "kmss3originoacDefaultTestDeployAssert38960359": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "kmss3originoacDefaultTestDeployAssert38960359.template.json", + "terminationProtection": false, + "validateOnSynth": false, + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", + "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", + "additionalDependencies": [ + "kmss3originoacDefaultTestDeployAssert38960359.assets" + ], + "lookupRole": { + "arn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}", + "requiresBootstrapStackVersion": 8, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "dependencies": [ + "kmss3originoacDefaultTestDeployAssert38960359.assets" + ], + "metadata": { + "/kms-s3-origin-oac/DefaultTest/DeployAssert/BootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "BootstrapVersion" + } + ], + "/kms-s3-origin-oac/DefaultTest/DeployAssert/CheckBootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "CheckBootstrapVersion" + } + ] + }, + "displayName": "kms-s3-origin-oac/DefaultTest/DeployAssert" + }, + "Tree": { + "type": "cdk:tree", + "properties": { + "file": "tree.json" + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront-origins/test/integ.s3-origin-oac-ssekms.js.snapshot/ssekms-s3-origin-oac.assets.json b/packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront-origins/test/integ.s3-origin-oac-ssekms.js.snapshot/ssekms-s3-origin-oac.assets.json new file mode 100644 index 0000000000000..8b2cf6cb917e3 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront-origins/test/integ.s3-origin-oac-ssekms.js.snapshot/ssekms-s3-origin-oac.assets.json @@ -0,0 +1,32 @@ +{ + "version": "36.0.0", + "files": { + "b1d5dd3578b58b3875b24727941d7b7ddc396de56dd9734b76c56a93c115c449": { + "source": { + "path": "asset.b1d5dd3578b58b3875b24727941d7b7ddc396de56dd9734b76c56a93c115c449", + "packaging": "zip" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "b1d5dd3578b58b3875b24727941d7b7ddc396de56dd9734b76c56a93c115c449.zip", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + }, + "8e090bce96f44f628ab8baaa9bd7db50201c5df8945bf80d5737fbd954753e59": { + "source": { + "path": "ssekms-s3-origin-oac.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "8e090bce96f44f628ab8baaa9bd7db50201c5df8945bf80d5737fbd954753e59.json", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + } + }, + "dockerImages": {} +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront-origins/test/integ.s3-origin-oac-ssekms.js.snapshot/ssekms-s3-origin-oac.template.json b/packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront-origins/test/integ.s3-origin-oac-ssekms.js.snapshot/ssekms-s3-origin-oac.template.json new file mode 100644 index 0000000000000..9d78a4078fbee --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront-origins/test/integ.s3-origin-oac-ssekms.js.snapshot/ssekms-s3-origin-oac.template.json @@ -0,0 +1,418 @@ +{ + "Resources": { + "Key961B73FD": { + "Type": "AWS::KMS::Key", + "Properties": { + "KeyPolicy": { + "Statement": [ + { + "Action": "kms:*", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":root" + ] + ] + } + }, + "Resource": "*" + } + ], + "Version": "2012-10-17" + } + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "Bucket83908E77": { + "Type": "AWS::S3::Bucket", + "Properties": { + "BucketEncryption": { + "ServerSideEncryptionConfiguration": [ + { + "ServerSideEncryptionByDefault": { + "KMSMasterKeyID": { + "Fn::GetAtt": [ + "Key961B73FD", + "Arn" + ] + }, + "SSEAlgorithm": "aws:kms" + } + } + ] + } + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "BucketPolicyE9A3008A": { + "Type": "AWS::S3::BucketPolicy", + "Properties": { + "Bucket": { + "Ref": "Bucket83908E77" + }, + "PolicyDocument": { + "Statement": [ + { + "Action": "s3:GetObject", + "Condition": { + "StringEquals": { + "AWS:SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":cloudfront::", + { + "Ref": "AWS::AccountId" + }, + ":distribution/", + { + "Ref": "Distribution830FAC52" + } + ] + ] + } + } + }, + "Effect": "Allow", + "Principal": { + "Service": "cloudfront.amazonaws.com" + }, + "Resource": { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "Bucket83908E77", + "Arn" + ] + }, + "/*" + ] + ] + }, + "Sid": "GrantOACAccessToS3" + } + ], + "Version": "2012-10-17" + } + } + }, + "OriginAccessControlDC2011A2": { + "Type": "AWS::CloudFront::OriginAccessControl", + "Properties": { + "OriginAccessControlConfig": { + "Name": "ssekmss3originoacOriginAccessControl41BE8C71", + "OriginAccessControlOriginType": "s3", + "SigningBehavior": "always", + "SigningProtocol": "sigv4" + } + } + }, + "DistributionOrigin1S3OriginKMSKeyPolicyCustomResource29A87268": { + "Type": "Custom::S3OriginAccessControlKeyPolicyUpdater", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "CustomS3OriginAccessControlKeyPolicyUpdaterCustomResourceProviderHandler6A458930", + "Arn" + ] + }, + "DistributionId": { + "Ref": "Distribution830FAC52" + }, + "KmsKeyId": { + "Ref": "Key961B73FD" + }, + "AccountId": { + "Ref": "AWS::AccountId" + }, + "Partition": { + "Ref": "AWS::Partition" + }, + "AccessLevels": [ + "READ" + ] + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "Distribution830FAC52": { + "Type": "AWS::CloudFront::Distribution", + "Properties": { + "DistributionConfig": { + "DefaultCacheBehavior": { + "CachePolicyId": "658327ea-f89d-4fab-a63d-7e88639e58f6", + "Compress": true, + "TargetOriginId": "ssekmss3originoacDistributionOrigin15B38431E", + "ViewerProtocolPolicy": "allow-all" + }, + "Enabled": true, + "HttpVersion": "http2", + "IPV6Enabled": true, + "Origins": [ + { + "DomainName": { + "Fn::GetAtt": [ + "Bucket83908E77", + "RegionalDomainName" + ] + }, + "Id": "ssekmss3originoacDistributionOrigin15B38431E", + "OriginAccessControlId": { + "Fn::GetAtt": [ + "OriginAccessControlDC2011A2", + "Id" + ] + }, + "S3OriginConfig": { + "OriginAccessIdentity": "" + } + } + ] + } + } + }, + "CustomS3OriginAccessControlKeyPolicyUpdaterCustomResourceProviderRole721641A7": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ] + }, + "ManagedPolicyArns": [ + { + "Fn::Sub": "arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + } + ], + "Policies": [ + { + "PolicyName": "Inline", + "PolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "kms:PutKeyPolicy", + "kms:GetKeyPolicy", + "kms:DescribeKey" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "Key961B73FD", + "Arn" + ] + } + ] + } + ] + } + } + ] + } + }, + "CustomS3OriginAccessControlKeyPolicyUpdaterCustomResourceProviderHandler6A458930": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" + }, + "S3Key": "b1d5dd3578b58b3875b24727941d7b7ddc396de56dd9734b76c56a93c115c449.zip" + }, + "Timeout": 900, + "MemorySize": 128, + "Handler": "__entrypoint__.handler", + "Role": { + "Fn::GetAtt": [ + "CustomS3OriginAccessControlKeyPolicyUpdaterCustomResourceProviderRole721641A7", + "Arn" + ] + }, + "Runtime": { + "Fn::FindInMap": [ + "LatestNodeRuntimeMap", + { + "Ref": "AWS::Region" + }, + "value" + ] + }, + "Description": "Lambda function that updates SSE-KMS key policy to allow CloudFront distribution access." + }, + "DependsOn": [ + "CustomS3OriginAccessControlKeyPolicyUpdaterCustomResourceProviderRole721641A7" + ] + } + }, + "Mappings": { + "LatestNodeRuntimeMap": { + "af-south-1": { + "value": "nodejs20.x" + }, + "ap-east-1": { + "value": "nodejs20.x" + }, + "ap-northeast-1": { + "value": "nodejs20.x" + }, + "ap-northeast-2": { + "value": "nodejs20.x" + }, + "ap-northeast-3": { + "value": "nodejs20.x" + }, + "ap-south-1": { + "value": "nodejs20.x" + }, + "ap-south-2": { + "value": "nodejs20.x" + }, + "ap-southeast-1": { + "value": "nodejs20.x" + }, + "ap-southeast-2": { + "value": "nodejs20.x" + }, + "ap-southeast-3": { + "value": "nodejs20.x" + }, + "ap-southeast-4": { + "value": "nodejs20.x" + }, + "ca-central-1": { + "value": "nodejs20.x" + }, + "cn-north-1": { + "value": "nodejs18.x" + }, + "cn-northwest-1": { + "value": "nodejs18.x" + }, + "eu-central-1": { + "value": "nodejs20.x" + }, + "eu-central-2": { + "value": "nodejs20.x" + }, + "eu-north-1": { + "value": "nodejs20.x" + }, + "eu-south-1": { + "value": "nodejs20.x" + }, + "eu-south-2": { + "value": "nodejs20.x" + }, + "eu-west-1": { + "value": "nodejs20.x" + }, + "eu-west-2": { + "value": "nodejs20.x" + }, + "eu-west-3": { + "value": "nodejs20.x" + }, + "il-central-1": { + "value": "nodejs20.x" + }, + "me-central-1": { + "value": "nodejs20.x" + }, + "me-south-1": { + "value": "nodejs20.x" + }, + "sa-east-1": { + "value": "nodejs20.x" + }, + "us-east-1": { + "value": "nodejs20.x" + }, + "us-east-2": { + "value": "nodejs20.x" + }, + "us-gov-east-1": { + "value": "nodejs18.x" + }, + "us-gov-west-1": { + "value": "nodejs18.x" + }, + "us-iso-east-1": { + "value": "nodejs18.x" + }, + "us-iso-west-1": { + "value": "nodejs18.x" + }, + "us-isob-east-1": { + "value": "nodejs18.x" + }, + "us-west-1": { + "value": "nodejs20.x" + }, + "us-west-2": { + "value": "nodejs20.x" + } + } + }, + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront-origins/test/integ.s3-origin-oac-ssekms.js.snapshot/tree.json b/packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront-origins/test/integ.s3-origin-oac-ssekms.js.snapshot/tree.json new file mode 100644 index 0000000000000..02506373e22d0 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront-origins/test/integ.s3-origin-oac-ssekms.js.snapshot/tree.json @@ -0,0 +1,420 @@ +{ + "version": "tree-0.1", + "tree": { + "id": "App", + "path": "", + "children": { + "ssekms-s3-origin-oac": { + "id": "ssekms-s3-origin-oac", + "path": "ssekms-s3-origin-oac", + "children": { + "Key": { + "id": "Key", + "path": "ssekms-s3-origin-oac/Key", + "children": { + "Resource": { + "id": "Resource", + "path": "ssekms-s3-origin-oac/Key/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::KMS::Key", + "aws:cdk:cloudformation:props": { + "keyPolicy": { + "Statement": [ + { + "Action": "kms:*", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":root" + ] + ] + } + }, + "Resource": "*" + } + ], + "Version": "2012-10-17" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_kms.CfnKey", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_kms.Key", + "version": "0.0.0" + } + }, + "Bucket": { + "id": "Bucket", + "path": "ssekms-s3-origin-oac/Bucket", + "children": { + "Resource": { + "id": "Resource", + "path": "ssekms-s3-origin-oac/Bucket/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::S3::Bucket", + "aws:cdk:cloudformation:props": { + "bucketEncryption": { + "serverSideEncryptionConfiguration": [ + { + "serverSideEncryptionByDefault": { + "sseAlgorithm": "aws:kms", + "kmsMasterKeyId": { + "Fn::GetAtt": [ + "Key961B73FD", + "Arn" + ] + } + } + } + ] + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_s3.CfnBucket", + "version": "0.0.0" + } + }, + "Policy": { + "id": "Policy", + "path": "ssekms-s3-origin-oac/Bucket/Policy", + "children": { + "Resource": { + "id": "Resource", + "path": "ssekms-s3-origin-oac/Bucket/Policy/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::S3::BucketPolicy", + "aws:cdk:cloudformation:props": { + "bucket": { + "Ref": "Bucket83908E77" + }, + "policyDocument": { + "Statement": [ + { + "Action": "s3:GetObject", + "Condition": { + "StringEquals": { + "AWS:SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":cloudfront::", + { + "Ref": "AWS::AccountId" + }, + ":distribution/", + { + "Ref": "Distribution830FAC52" + } + ] + ] + } + } + }, + "Effect": "Allow", + "Principal": { + "Service": "cloudfront.amazonaws.com" + }, + "Resource": { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "Bucket83908E77", + "Arn" + ] + }, + "/*" + ] + ] + }, + "Sid": "GrantOACAccessToS3" + } + ], + "Version": "2012-10-17" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_s3.CfnBucketPolicy", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_s3.BucketPolicy", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_s3.Bucket", + "version": "0.0.0" + } + }, + "OriginAccessControl": { + "id": "OriginAccessControl", + "path": "ssekms-s3-origin-oac/OriginAccessControl", + "children": { + "Resource": { + "id": "Resource", + "path": "ssekms-s3-origin-oac/OriginAccessControl/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::CloudFront::OriginAccessControl", + "aws:cdk:cloudformation:props": { + "originAccessControlConfig": { + "name": "ssekmss3originoacOriginAccessControl41BE8C71", + "signingBehavior": "always", + "signingProtocol": "sigv4", + "originAccessControlOriginType": "s3" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_cloudfront.CfnOriginAccessControl", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_cloudfront.OriginAccessControl", + "version": "0.0.0" + } + }, + "Distribution": { + "id": "Distribution", + "path": "ssekms-s3-origin-oac/Distribution", + "children": { + "Origin1": { + "id": "Origin1", + "path": "ssekms-s3-origin-oac/Distribution/Origin1", + "children": { + "S3OriginKMSKeyPolicyCustomResource": { + "id": "S3OriginKMSKeyPolicyCustomResource", + "path": "ssekms-s3-origin-oac/Distribution/Origin1/S3OriginKMSKeyPolicyCustomResource", + "children": { + "Default": { + "id": "Default", + "path": "ssekms-s3-origin-oac/Distribution/Origin1/S3OriginKMSKeyPolicyCustomResource/Default", + "constructInfo": { + "fqn": "aws-cdk-lib.CfnResource", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.CustomResource", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + }, + "Resource": { + "id": "Resource", + "path": "ssekms-s3-origin-oac/Distribution/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::CloudFront::Distribution", + "aws:cdk:cloudformation:props": { + "distributionConfig": { + "enabled": true, + "origins": [ + { + "domainName": { + "Fn::GetAtt": [ + "Bucket83908E77", + "RegionalDomainName" + ] + }, + "id": "ssekmss3originoacDistributionOrigin15B38431E", + "s3OriginConfig": { + "originAccessIdentity": "" + }, + "originAccessControlId": { + "Fn::GetAtt": [ + "OriginAccessControlDC2011A2", + "Id" + ] + } + } + ], + "defaultCacheBehavior": { + "pathPattern": "*", + "targetOriginId": "ssekmss3originoacDistributionOrigin15B38431E", + "cachePolicyId": "658327ea-f89d-4fab-a63d-7e88639e58f6", + "compress": true, + "viewerProtocolPolicy": "allow-all" + }, + "httpVersion": "http2", + "ipv6Enabled": true + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_cloudfront.CfnDistribution", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_cloudfront.Distribution", + "version": "0.0.0" + } + }, + "LatestNodeRuntimeMap": { + "id": "LatestNodeRuntimeMap", + "path": "ssekms-s3-origin-oac/LatestNodeRuntimeMap", + "constructInfo": { + "fqn": "aws-cdk-lib.CfnMapping", + "version": "0.0.0" + } + }, + "Custom::S3OriginAccessControlKeyPolicyUpdaterCustomResourceProvider": { + "id": "Custom::S3OriginAccessControlKeyPolicyUpdaterCustomResourceProvider", + "path": "ssekms-s3-origin-oac/Custom::S3OriginAccessControlKeyPolicyUpdaterCustomResourceProvider", + "children": { + "Staging": { + "id": "Staging", + "path": "ssekms-s3-origin-oac/Custom::S3OriginAccessControlKeyPolicyUpdaterCustomResourceProvider/Staging", + "constructInfo": { + "fqn": "aws-cdk-lib.AssetStaging", + "version": "0.0.0" + } + }, + "Role": { + "id": "Role", + "path": "ssekms-s3-origin-oac/Custom::S3OriginAccessControlKeyPolicyUpdaterCustomResourceProvider/Role", + "constructInfo": { + "fqn": "aws-cdk-lib.CfnResource", + "version": "0.0.0" + } + }, + "Handler": { + "id": "Handler", + "path": "ssekms-s3-origin-oac/Custom::S3OriginAccessControlKeyPolicyUpdaterCustomResourceProvider/Handler", + "constructInfo": { + "fqn": "aws-cdk-lib.CfnResource", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.CustomResourceProviderBase", + "version": "0.0.0" + } + }, + "BootstrapVersion": { + "id": "BootstrapVersion", + "path": "ssekms-s3-origin-oac/BootstrapVersion", + "constructInfo": { + "fqn": "aws-cdk-lib.CfnParameter", + "version": "0.0.0" + } + }, + "CheckBootstrapVersion": { + "id": "CheckBootstrapVersion", + "path": "ssekms-s3-origin-oac/CheckBootstrapVersion", + "constructInfo": { + "fqn": "aws-cdk-lib.CfnRule", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.Stack", + "version": "0.0.0" + } + }, + "kms-s3-origin-oac": { + "id": "kms-s3-origin-oac", + "path": "kms-s3-origin-oac", + "children": { + "DefaultTest": { + "id": "DefaultTest", + "path": "kms-s3-origin-oac/DefaultTest", + "children": { + "Default": { + "id": "Default", + "path": "kms-s3-origin-oac/DefaultTest/Default", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + }, + "DeployAssert": { + "id": "DeployAssert", + "path": "kms-s3-origin-oac/DefaultTest/DeployAssert", + "children": { + "BootstrapVersion": { + "id": "BootstrapVersion", + "path": "kms-s3-origin-oac/DefaultTest/DeployAssert/BootstrapVersion", + "constructInfo": { + "fqn": "aws-cdk-lib.CfnParameter", + "version": "0.0.0" + } + }, + "CheckBootstrapVersion": { + "id": "CheckBootstrapVersion", + "path": "kms-s3-origin-oac/DefaultTest/DeployAssert/CheckBootstrapVersion", + "constructInfo": { + "fqn": "aws-cdk-lib.CfnRule", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.Stack", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests-alpha.IntegTestCase", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests-alpha.IntegTest", + "version": "0.0.0" + } + }, + "Tree": { + "id": "Tree", + "path": "Tree", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.App", + "version": "0.0.0" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront-origins/test/integ.s3-origin-oac-ssekms.ts b/packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront-origins/test/integ.s3-origin-oac-ssekms.ts new file mode 100644 index 0000000000000..892b14ec8dd18 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront-origins/test/integ.s3-origin-oac-ssekms.ts @@ -0,0 +1,28 @@ +import * as cloudfront from 'aws-cdk-lib/aws-cloudfront'; +import * as s3 from 'aws-cdk-lib/aws-s3'; +import * as kms from 'aws-cdk-lib/aws-kms'; +import * as cdk from 'aws-cdk-lib'; +import * as origins from 'aws-cdk-lib/aws-cloudfront-origins'; +import { IntegTest } from '@aws-cdk/integ-tests-alpha'; + +const app = new cdk.App(); + +const stack = new cdk.Stack(app, 'ssekms-s3-origin-oac'); + +const key = new kms.Key(stack, 'Key'); +const bucket = new s3.Bucket(stack, 'Bucket', { + encryptionKey: key, + encryption: s3.BucketEncryption.KMS, +}); +const originAccessControl = new cloudfront.OriginAccessControl(stack, 'OriginAccessControl'); +new cloudfront.Distribution(stack, 'Distribution', { + defaultBehavior: { + origin: new origins.S3Origin(bucket, { + originAccessControl: originAccessControl + }) + }, +}); + +new IntegTest(app, 'kms-s3-origin-oac', { + testCases: [stack], +}); diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront-origins/test/integ.s3-origin-oac.js.snapshot/cdk.out b/packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront-origins/test/integ.s3-origin-oac.js.snapshot/cdk.out new file mode 100644 index 0000000000000..1f0068d32659a --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront-origins/test/integ.s3-origin-oac.js.snapshot/cdk.out @@ -0,0 +1 @@ +{"version":"36.0.0"} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront-origins/test/integ.s3-origin-oac.js.snapshot/cloudfront-s3-origin-oac.assets.json b/packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront-origins/test/integ.s3-origin-oac.js.snapshot/cloudfront-s3-origin-oac.assets.json new file mode 100644 index 0000000000000..971b8a03b2416 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront-origins/test/integ.s3-origin-oac.js.snapshot/cloudfront-s3-origin-oac.assets.json @@ -0,0 +1,19 @@ +{ + "version": "36.0.0", + "files": { + "271f902df853287785b545e1192fe8ec9c8088dc5df553520608bbe6a5d8a76d": { + "source": { + "path": "cloudfront-s3-origin-oac.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "271f902df853287785b545e1192fe8ec9c8088dc5df553520608bbe6a5d8a76d.json", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + } + }, + "dockerImages": {} +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront-origins/test/integ.s3-origin-oac.js.snapshot/cloudfront-s3-origin-oac.template.json b/packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront-origins/test/integ.s3-origin-oac.js.snapshot/cloudfront-s3-origin-oac.template.json new file mode 100644 index 0000000000000..95a110768878b --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront-origins/test/integ.s3-origin-oac.js.snapshot/cloudfront-s3-origin-oac.template.json @@ -0,0 +1,148 @@ +{ + "Resources": { + "Bucket83908E77": { + "Type": "AWS::S3::Bucket", + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "BucketPolicyE9A3008A": { + "Type": "AWS::S3::BucketPolicy", + "Properties": { + "Bucket": { + "Ref": "Bucket83908E77" + }, + "PolicyDocument": { + "Statement": [ + { + "Action": "s3:GetObject", + "Condition": { + "StringEquals": { + "AWS:SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":cloudfront::", + { + "Ref": "AWS::AccountId" + }, + ":distribution/", + { + "Ref": "Distribution830FAC52" + } + ] + ] + } + } + }, + "Effect": "Allow", + "Principal": { + "Service": "cloudfront.amazonaws.com" + }, + "Resource": { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "Bucket83908E77", + "Arn" + ] + }, + "/*" + ] + ] + }, + "Sid": "GrantOACAccessToS3" + } + ], + "Version": "2012-10-17" + } + } + }, + "OriginAccessControlDC2011A2": { + "Type": "AWS::CloudFront::OriginAccessControl", + "Properties": { + "OriginAccessControlConfig": { + "Name": "cloudfronts3originoacOriginAccessControl90512E67", + "OriginAccessControlOriginType": "s3", + "SigningBehavior": "always", + "SigningProtocol": "sigv4" + } + } + }, + "Distribution830FAC52": { + "Type": "AWS::CloudFront::Distribution", + "Properties": { + "DistributionConfig": { + "DefaultCacheBehavior": { + "CachePolicyId": "658327ea-f89d-4fab-a63d-7e88639e58f6", + "Compress": true, + "TargetOriginId": "cloudfronts3originoacDistributionOrigin12695E2FC", + "ViewerProtocolPolicy": "allow-all" + }, + "Enabled": true, + "HttpVersion": "http2", + "IPV6Enabled": true, + "Origins": [ + { + "DomainName": { + "Fn::GetAtt": [ + "Bucket83908E77", + "RegionalDomainName" + ] + }, + "Id": "cloudfronts3originoacDistributionOrigin12695E2FC", + "OriginAccessControlId": { + "Fn::GetAtt": [ + "OriginAccessControlDC2011A2", + "Id" + ] + }, + "S3OriginConfig": { + "OriginAccessIdentity": "" + } + } + ] + } + } + } + }, + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront-origins/test/integ.s3-origin-oac.js.snapshot/integ.json b/packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront-origins/test/integ.s3-origin-oac.js.snapshot/integ.json new file mode 100644 index 0000000000000..f142e52f9e416 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront-origins/test/integ.s3-origin-oac.js.snapshot/integ.json @@ -0,0 +1,12 @@ +{ + "version": "36.0.0", + "testCases": { + "s3-origin-oac/DefaultTest": { + "stacks": [ + "cloudfront-s3-origin-oac" + ], + "assertionStack": "s3-origin-oac/DefaultTest/DeployAssert", + "assertionStackName": "s3originoacDefaultTestDeployAssertD8FD270A" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront-origins/test/integ.s3-origin-oac.js.snapshot/manifest.json b/packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront-origins/test/integ.s3-origin-oac.js.snapshot/manifest.json new file mode 100644 index 0000000000000..d50ae4893a8ca --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront-origins/test/integ.s3-origin-oac.js.snapshot/manifest.json @@ -0,0 +1,131 @@ +{ + "version": "36.0.0", + "artifacts": { + "cloudfront-s3-origin-oac.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "cloudfront-s3-origin-oac.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "cloudfront-s3-origin-oac": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "cloudfront-s3-origin-oac.template.json", + "terminationProtection": false, + "validateOnSynth": false, + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", + "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/271f902df853287785b545e1192fe8ec9c8088dc5df553520608bbe6a5d8a76d.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", + "additionalDependencies": [ + "cloudfront-s3-origin-oac.assets" + ], + "lookupRole": { + "arn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}", + "requiresBootstrapStackVersion": 8, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "dependencies": [ + "cloudfront-s3-origin-oac.assets" + ], + "metadata": { + "/cloudfront-s3-origin-oac/Bucket/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "Bucket83908E77" + } + ], + "/cloudfront-s3-origin-oac/Bucket/Policy/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "BucketPolicyE9A3008A" + } + ], + "/cloudfront-s3-origin-oac/OriginAccessControl/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "OriginAccessControlDC2011A2" + } + ], + "/cloudfront-s3-origin-oac/Distribution/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "Distribution830FAC52" + } + ], + "/cloudfront-s3-origin-oac/BootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "BootstrapVersion" + } + ], + "/cloudfront-s3-origin-oac/CheckBootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "CheckBootstrapVersion" + } + ] + }, + "displayName": "cloudfront-s3-origin-oac" + }, + "s3originoacDefaultTestDeployAssertD8FD270A.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "s3originoacDefaultTestDeployAssertD8FD270A.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "s3originoacDefaultTestDeployAssertD8FD270A": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "s3originoacDefaultTestDeployAssertD8FD270A.template.json", + "terminationProtection": false, + "validateOnSynth": false, + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", + "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", + "additionalDependencies": [ + "s3originoacDefaultTestDeployAssertD8FD270A.assets" + ], + "lookupRole": { + "arn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}", + "requiresBootstrapStackVersion": 8, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "dependencies": [ + "s3originoacDefaultTestDeployAssertD8FD270A.assets" + ], + "metadata": { + "/s3-origin-oac/DefaultTest/DeployAssert/BootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "BootstrapVersion" + } + ], + "/s3-origin-oac/DefaultTest/DeployAssert/CheckBootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "CheckBootstrapVersion" + } + ] + }, + "displayName": "s3-origin-oac/DefaultTest/DeployAssert" + }, + "Tree": { + "type": "cdk:tree", + "properties": { + "file": "tree.json" + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront-origins/test/integ.s3-origin-oac.js.snapshot/s3originoacDefaultTestDeployAssertD8FD270A.assets.json b/packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront-origins/test/integ.s3-origin-oac.js.snapshot/s3originoacDefaultTestDeployAssertD8FD270A.assets.json new file mode 100644 index 0000000000000..439c8d34e9f48 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront-origins/test/integ.s3-origin-oac.js.snapshot/s3originoacDefaultTestDeployAssertD8FD270A.assets.json @@ -0,0 +1,19 @@ +{ + "version": "36.0.0", + "files": { + "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22": { + "source": { + "path": "s3originoacDefaultTestDeployAssertD8FD270A.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22.json", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + } + }, + "dockerImages": {} +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront-origins/test/integ.s3-origin-oac.js.snapshot/s3originoacDefaultTestDeployAssertD8FD270A.template.json b/packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront-origins/test/integ.s3-origin-oac.js.snapshot/s3originoacDefaultTestDeployAssertD8FD270A.template.json new file mode 100644 index 0000000000000..ad9d0fb73d1dd --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront-origins/test/integ.s3-origin-oac.js.snapshot/s3originoacDefaultTestDeployAssertD8FD270A.template.json @@ -0,0 +1,36 @@ +{ + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront-origins/test/integ.s3-origin-oac.js.snapshot/tree.json b/packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront-origins/test/integ.s3-origin-oac.js.snapshot/tree.json new file mode 100644 index 0000000000000..0245e03ddff3b --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront-origins/test/integ.s3-origin-oac.js.snapshot/tree.json @@ -0,0 +1,291 @@ +{ + "version": "tree-0.1", + "tree": { + "id": "App", + "path": "", + "children": { + "cloudfront-s3-origin-oac": { + "id": "cloudfront-s3-origin-oac", + "path": "cloudfront-s3-origin-oac", + "children": { + "Bucket": { + "id": "Bucket", + "path": "cloudfront-s3-origin-oac/Bucket", + "children": { + "Resource": { + "id": "Resource", + "path": "cloudfront-s3-origin-oac/Bucket/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::S3::Bucket", + "aws:cdk:cloudformation:props": {} + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_s3.CfnBucket", + "version": "0.0.0" + } + }, + "Policy": { + "id": "Policy", + "path": "cloudfront-s3-origin-oac/Bucket/Policy", + "children": { + "Resource": { + "id": "Resource", + "path": "cloudfront-s3-origin-oac/Bucket/Policy/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::S3::BucketPolicy", + "aws:cdk:cloudformation:props": { + "bucket": { + "Ref": "Bucket83908E77" + }, + "policyDocument": { + "Statement": [ + { + "Action": "s3:GetObject", + "Condition": { + "StringEquals": { + "AWS:SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":cloudfront::", + { + "Ref": "AWS::AccountId" + }, + ":distribution/", + { + "Ref": "Distribution830FAC52" + } + ] + ] + } + } + }, + "Effect": "Allow", + "Principal": { + "Service": "cloudfront.amazonaws.com" + }, + "Resource": { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "Bucket83908E77", + "Arn" + ] + }, + "/*" + ] + ] + }, + "Sid": "GrantOACAccessToS3" + } + ], + "Version": "2012-10-17" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_s3.CfnBucketPolicy", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_s3.BucketPolicy", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_s3.Bucket", + "version": "0.0.0" + } + }, + "OriginAccessControl": { + "id": "OriginAccessControl", + "path": "cloudfront-s3-origin-oac/OriginAccessControl", + "children": { + "Resource": { + "id": "Resource", + "path": "cloudfront-s3-origin-oac/OriginAccessControl/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::CloudFront::OriginAccessControl", + "aws:cdk:cloudformation:props": { + "originAccessControlConfig": { + "name": "cloudfronts3originoacOriginAccessControl90512E67", + "signingBehavior": "always", + "signingProtocol": "sigv4", + "originAccessControlOriginType": "s3" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_cloudfront.CfnOriginAccessControl", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_cloudfront.OriginAccessControl", + "version": "0.0.0" + } + }, + "Distribution": { + "id": "Distribution", + "path": "cloudfront-s3-origin-oac/Distribution", + "children": { + "Origin1": { + "id": "Origin1", + "path": "cloudfront-s3-origin-oac/Distribution/Origin1", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + }, + "Resource": { + "id": "Resource", + "path": "cloudfront-s3-origin-oac/Distribution/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::CloudFront::Distribution", + "aws:cdk:cloudformation:props": { + "distributionConfig": { + "enabled": true, + "origins": [ + { + "domainName": { + "Fn::GetAtt": [ + "Bucket83908E77", + "RegionalDomainName" + ] + }, + "id": "cloudfronts3originoacDistributionOrigin12695E2FC", + "s3OriginConfig": { + "originAccessIdentity": "" + }, + "originAccessControlId": { + "Fn::GetAtt": [ + "OriginAccessControlDC2011A2", + "Id" + ] + } + } + ], + "defaultCacheBehavior": { + "pathPattern": "*", + "targetOriginId": "cloudfronts3originoacDistributionOrigin12695E2FC", + "cachePolicyId": "658327ea-f89d-4fab-a63d-7e88639e58f6", + "compress": true, + "viewerProtocolPolicy": "allow-all" + }, + "httpVersion": "http2", + "ipv6Enabled": true + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_cloudfront.CfnDistribution", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_cloudfront.Distribution", + "version": "0.0.0" + } + }, + "BootstrapVersion": { + "id": "BootstrapVersion", + "path": "cloudfront-s3-origin-oac/BootstrapVersion", + "constructInfo": { + "fqn": "aws-cdk-lib.CfnParameter", + "version": "0.0.0" + } + }, + "CheckBootstrapVersion": { + "id": "CheckBootstrapVersion", + "path": "cloudfront-s3-origin-oac/CheckBootstrapVersion", + "constructInfo": { + "fqn": "aws-cdk-lib.CfnRule", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.Stack", + "version": "0.0.0" + } + }, + "s3-origin-oac": { + "id": "s3-origin-oac", + "path": "s3-origin-oac", + "children": { + "DefaultTest": { + "id": "DefaultTest", + "path": "s3-origin-oac/DefaultTest", + "children": { + "Default": { + "id": "Default", + "path": "s3-origin-oac/DefaultTest/Default", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + }, + "DeployAssert": { + "id": "DeployAssert", + "path": "s3-origin-oac/DefaultTest/DeployAssert", + "children": { + "BootstrapVersion": { + "id": "BootstrapVersion", + "path": "s3-origin-oac/DefaultTest/DeployAssert/BootstrapVersion", + "constructInfo": { + "fqn": "aws-cdk-lib.CfnParameter", + "version": "0.0.0" + } + }, + "CheckBootstrapVersion": { + "id": "CheckBootstrapVersion", + "path": "s3-origin-oac/DefaultTest/DeployAssert/CheckBootstrapVersion", + "constructInfo": { + "fqn": "aws-cdk-lib.CfnRule", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.Stack", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests-alpha.IntegTestCase", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests-alpha.IntegTest", + "version": "0.0.0" + } + }, + "Tree": { + "id": "Tree", + "path": "Tree", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.App", + "version": "0.0.0" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront-origins/test/integ.s3-origin-oac.ts b/packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront-origins/test/integ.s3-origin-oac.ts index e69de29bb2d1d..c8e3ecb3bf378 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront-origins/test/integ.s3-origin-oac.ts +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront-origins/test/integ.s3-origin-oac.ts @@ -0,0 +1,23 @@ +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 { IntegTest } from '@aws-cdk/integ-tests-alpha'; + +const app = new cdk.App(); + +const stack = new cdk.Stack(app, 'cloudfront-s3-origin-oac'); + +const bucket = new s3.Bucket(stack, 'Bucket'); +const originAccessControl = new cloudfront.OriginAccessControl(stack, 'OriginAccessControl'); +new cloudfront.Distribution(stack, 'Distribution', { + defaultBehavior: { + origin: new origins.S3Origin(bucket, { + originAccessControl: originAccessControl + }) + }, +}); + +new IntegTest(app, 's3-origin-oac', { + testCases: [stack], +}); diff --git a/packages/@aws-cdk/custom-resource-handlers/lib/aws-cloudfront-origins/s3-origin-access-control-bucket-policy-handler/index.ts b/packages/@aws-cdk/custom-resource-handlers/lib/aws-cloudfront-origins/s3-origin-access-control-bucket-policy-handler/index.ts index 521c96cabb0cf..fc25c1eeda0a4 100644 --- a/packages/@aws-cdk/custom-resource-handlers/lib/aws-cloudfront-origins/s3-origin-access-control-bucket-policy-handler/index.ts +++ b/packages/@aws-cdk/custom-resource-handlers/lib/aws-cloudfront-origins/s3-origin-access-control-bucket-policy-handler/index.ts @@ -38,6 +38,8 @@ export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent return { IsComplete: true, }; + } else { + return; } } @@ -64,7 +66,7 @@ export async function updateBucketPolicy(props: updateBucketPolicyProps) { }, }; // Give Origin Access Control permission to access the bucket - let updatedBucketPolicy = updatePolicy(policy, oacBucketPolicyStatement); + let updatedBucketPolicy = appendStatementToPolicy(policy, oacBucketPolicyStatement); console.log('Updated bucket policy', JSON.stringify(updatedBucketPolicy, undefined, 2)); await s3.putBucketPolicy({ @@ -93,7 +95,7 @@ export async function updateBucketPolicy(props: updateBucketPolicyProps) { * @param policyStatementToAdd - the policy statement to be added to the policy. * @returns currentPolicy - the updated policy. */ -export function updatePolicy(currentPolicy: any, policyStatementToAdd: any) { +export function appendStatementToPolicy(currentPolicy: any, policyStatementToAdd: any) { if (!isStatementInPolicy(currentPolicy, policyStatementToAdd)) { currentPolicy.Statement.push(policyStatementToAdd); } diff --git a/packages/@aws-cdk/custom-resource-handlers/lib/aws-cloudfront-origins/s3-origin-access-control-key-policy-handler/index.ts b/packages/@aws-cdk/custom-resource-handlers/lib/aws-cloudfront-origins/s3-origin-access-control-key-policy-handler/index.ts index f4a8b4d94bf3b..ebbbd3ba7f692 100644 --- a/packages/@aws-cdk/custom-resource-handlers/lib/aws-cloudfront-origins/s3-origin-access-control-key-policy-handler/index.ts +++ b/packages/@aws-cdk/custom-resource-handlers/lib/aws-cloudfront-origins/s3-origin-access-control-key-policy-handler/index.ts @@ -40,12 +40,12 @@ export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent throw new Error('An error occurred while retrieving the key policy.'); } - // Define the updated key policy to allow CloudFront Distribution access const keyPolicy = JSON.parse(getKeyPolicyCommandResponse?.Policy); console.log('Retrieved key policy', JSON.stringify(keyPolicy, undefined, 2)); - const actions = getActions(accessLevels); - + const actions = getActions(accessLevels) + + // Define the updated key policy to allow CloudFront Distribution access const kmsKeyPolicyStatement = { Sid: 'GrantOACAccessToKMS', Effect: 'Allow', @@ -63,7 +63,7 @@ export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent }, }; - const updatedKeyPolicy = updatePolicy(keyPolicy, kmsKeyPolicyStatement); + const updatedKeyPolicy = appendStatementToPolicy(keyPolicy, kmsKeyPolicyStatement); console.log('Updated key policy', JSON.stringify(updatedKeyPolicy, undefined, 2)); await kms.putKeyPolicy({ KeyId: kmsKeyId, @@ -108,6 +108,8 @@ export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent return { IsComplete: true, }; + } else { + return; } } @@ -127,7 +129,7 @@ export function getActions(accessLevels: string[]): string[] { * @param policyStatementToAdd - the policy statement to be added to the policy. * @returns currentPolicy - the updated policy. */ -export function updatePolicy(currentPolicy: any, policyStatementToAdd: any) { +export function appendStatementToPolicy(currentPolicy: any, policyStatementToAdd: any) { if (!isStatementInPolicy(currentPolicy, policyStatementToAdd)) { currentPolicy.Statement.push(policyStatementToAdd); } diff --git a/packages/@aws-cdk/custom-resource-handlers/test/aws-cloudfront-origins/s3-origin-access-control-bucket-policy-handler.test.ts b/packages/@aws-cdk/custom-resource-handlers/test/aws-cloudfront-origins/s3-origin-access-control-bucket-policy-handler.test.ts index fc6d2ea1f5332..1ab584939d679 100644 --- a/packages/@aws-cdk/custom-resource-handlers/test/aws-cloudfront-origins/s3-origin-access-control-bucket-policy-handler.test.ts +++ b/packages/@aws-cdk/custom-resource-handlers/test/aws-cloudfront-origins/s3-origin-access-control-bucket-policy-handler.test.ts @@ -1,7 +1,7 @@ import { S3Client, GetBucketPolicyCommand, PutBucketPolicyCommand } from '@aws-sdk/client-s3'; import { mockClient } from 'aws-sdk-client-mock'; import 'aws-sdk-client-mock-jest'; -import { handler, updatePolicy, isStatementInPolicy, isOaiPrincipal, removeOacPolicyStatement, removeOaiPolicyStatements, isOacPolicyStatement } from '../../lib/aws-cloudfront-origins/s3-origin-access-control-bucket-policy-handler/index'; +import { handler, appendStatementToPolicy, isStatementInPolicy, isOaiPrincipal, removeOacPolicyStatement, removeOaiPolicyStatements, isOacPolicyStatement } from '../../lib/aws-cloudfront-origins/s3-origin-access-control-bucket-policy-handler/index'; const s3Mock = mockClient(S3Client); @@ -60,7 +60,7 @@ describe('updatePolicy', () => { const currentPolicy = { Statement: [{ Sid: 'ExistingStatement', Effect: 'Allow', Action: 's3:GetObject', Resource: 'arn:aws:s3:::bucket/*' }] }; const policyStatementToAdd = { Sid: 'ExistingStatement', Effect: 'Allow', Action: 's3:GetObject', Resource: 'arn:aws:s3:::bucket/*' }; - const updatedPolicy = updatePolicy(currentPolicy, policyStatementToAdd); + const updatedPolicy = appendStatementToPolicy(currentPolicy, policyStatementToAdd); expect(updatedPolicy.Statement).toHaveLength(1); }); diff --git a/packages/@aws-cdk/custom-resource-handlers/test/aws-cloudfront-origins/s3-origin-access-control-key-policy-handler.test.ts b/packages/@aws-cdk/custom-resource-handlers/test/aws-cloudfront-origins/s3-origin-access-control-key-policy-handler.test.ts index 1ecf41ddd8ce0..e69c7b5b2e02e 100644 --- a/packages/@aws-cdk/custom-resource-handlers/test/aws-cloudfront-origins/s3-origin-access-control-key-policy-handler.test.ts +++ b/packages/@aws-cdk/custom-resource-handlers/test/aws-cloudfront-origins/s3-origin-access-control-key-policy-handler.test.ts @@ -1,7 +1,7 @@ import { KMS, KeyManagerType, DescribeKeyCommand, PutKeyPolicyCommand, GetKeyPolicyCommand } from '@aws-sdk/client-kms'; import { mockClient } from 'aws-sdk-client-mock'; import 'aws-sdk-client-mock-jest'; -import { handler, updatePolicy, getActions, isStatementInPolicy } from '../../lib/aws-cloudfront-origins/s3-origin-access-control-key-policy-handler/index'; +import { handler, appendStatementToPolicy, getActions, isStatementInPolicy } from '../../lib/aws-cloudfront-origins/s3-origin-access-control-key-policy-handler/index'; const kmsMock = mockClient(KMS); @@ -153,7 +153,7 @@ describe('updatePolicy', () => { it('should add a new policy statement if it does not exist', () => { const currentPolicy = { Statement: [] }; const policyStatementToAdd = { Sid: 'NewStatement', Effect: 'Allow', Action: ['kms:Decrypt'], Resource: '*' }; - const updatedPolicy = updatePolicy(currentPolicy, policyStatementToAdd); + const updatedPolicy = appendStatementToPolicy(currentPolicy, policyStatementToAdd); expect(updatedPolicy.Statement).toHaveLength(1); expect(updatedPolicy.Statement[0]).toEqual(policyStatementToAdd); }); @@ -165,7 +165,7 @@ describe('updatePolicy', () => { ], }; const policyStatementToAdd = { Sid: 'ExistingStatement', Effect: 'Allow', Action: ['kms:Decrypt'], Resource: '*' }; - const updatedPolicy = updatePolicy(currentPolicy, policyStatementToAdd); + const updatedPolicy = appendStatementToPolicy(currentPolicy, policyStatementToAdd); expect(updatedPolicy.Statement).toHaveLength(1); expect(updatedPolicy.Statement[0]).toEqual(policyStatementToAdd); }); diff --git a/packages/aws-cdk-lib/aws-cloudfront-origins/README.md b/packages/aws-cdk-lib/aws-cloudfront-origins/README.md index a5f2972f9c5d5..17fb304452c0b 100644 --- a/packages/aws-cdk-lib/aws-cloudfront-origins/README.md +++ b/packages/aws-cdk-lib/aws-cloudfront-origins/README.md @@ -18,9 +18,106 @@ new cloudfront.Distribution(this, 'myDist', { The above will treat the bucket differently based on if `IBucket.isWebsite` is set or not. If the bucket is configured as a website, the bucket is treated as an HTTP origin, and the built-in S3 redirects and error pages can be used. Otherwise, the bucket is handled as a bucket origin and -CloudFront's redirect and error handling will be used. In the latter case, the Origin will create an origin access identity and grant it access to the -underlying bucket. This can be used in conjunction with a bucket that is not public to require that your users access your content using CloudFront -URLs and not S3 URLs directly. Alternatively, a custom origin access identity can be passed to the S3 origin in the properties. +CloudFront's redirect and error handling will be used. + +### Restricting access to an S3 Origin + +CloudFront provides two ways to send authenticated requests to an Amazon S3 origin: origin access control (OAC) and origin access identity (OAI). +OAC is the recommended method and OAI is considered legacy (see [Restricting access to an Amazon Simple Storage Service origin](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-restricting-access-to-s3.html)). +Following AWS best practices, it is recommended you set the feature flag `@aws-cdk/aws-cloudfront:useOriginAccessControlByDefault` to `true` to use OAC by +default when creating new origins. + +For an S3 bucket that is configured as a standard S3 bucket origin (not as a website endpoint), when the above feature flag is enabled the `S3Origin` +construct will automatically create an OAC and grant it access (read-only by default) to the underlying bucket. + +> [Note](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-restricting-access-to-s3.html): When you use OAC with S3 +bucket origins you must set the bucket's object ownership to Bucket owner enforced, or Bucket owner preferred (only if you require ACLs). + +```ts +const myBucket = new s3.Bucket(this, 'myBucket', { + objectOwnership: s3.ObjectOwnership.BUCKET_OWNER_ENFORCED, +}); +new cloudfront.Distribution(this, 'myDist', { + defaultBehavior: { + origin: new origins.S3Origin(myBucket) // Automatically creates an OAC + }, +}); +``` + +Alternatively, a custom origin access control can be passed to the S3 origin: + +```ts +const myBucket = new s3.Bucket(this, 'myBucket'); +const myOAC = new cloudfront.OriginAccessControl(this, 'myOAC', { + description: 'Origin access control for S3 origin', + originAccessControlOriginType: cloudfront.OriginAccessControlOriginType.S3, +}); +new cloudfront.Distribution(this, 'myDist', { + defaultBehavior: { + origin: new origins.S3Origin(myBucket, { + originAccessControl: myOAC + }), + }, +}); +``` + +Alternatively, an existing origin access control can be imported: + +```ts +const myBucket = new s3.Bucket(this, 'myBucket'); +const importedOAC = cloudfront.OriginAccessControl.fromOriginAccessControlAttributes(this, 'myImportedOAC', { + originAccessControlId: 'ABC123ABC123AB', + originAccessControlOriginType: cloudfront.OriginAccessControlOriginType.S3, +}); +new cloudfront.Distribution(this, 'myDist', { + defaultBehavior: { + origin: new origins.S3Origin(myBucket, { + originAccessControl: importedOAC + }), + }, +}); +``` + +If the feature flag is not enabled (i.e. set to `false`), an origin access identity will be created by default. + +#### Using OAC for a SSE-KMS encrypted S3 origin + +If the objects in the S3 bucket origin are encrypted using server-side encryption with +AWS Key Management Service (SSE-KMS), the OAC must have permission to use the AWS KMS key. + +```ts +const myKmsKey = new kms.Key(this, 'myKMSKey'); +const myBucket = new s3.Bucket(this, 'mySSEKMSEncryptedBucket', { + encryption: s3.BucketEncryption.KMS, + encryptionKey: kmsKey, +}); +new cloudfront.Distribution(this, 'myDist', { + defaultBehavior: { + origin: new origins.S3Origin(myBucket) // Automatically creates an OAC + }, +}); +``` +If the S3 bucket has an `encryptionKey` defined, the `S3Origin` construct +will update the KMS key policy by appending the following policy statement to allow CloudFront read-only access (unless otherwise specified in the `originAccessLevels` property): + +``` +{ + "Statement": { + "Sid": "GrantOACAccessToKMS", + "Effect": "Allow", + "Principal": { + "Service": "cloudfront.amazonaws.com" + }, + "Action": "kms:Decrypt", + "Resource": "arn:aws:kms:::key/", + "Condition": { + "StringEquals": { + "AWS:SourceArn": "arn:aws:cloudfront::111122223333:distribution/" + } + } + } +} +``` ### Adding Custom Headers diff --git a/packages/aws-cdk-lib/aws-cloudfront-origins/lib/s3-origin.ts b/packages/aws-cdk-lib/aws-cloudfront-origins/lib/s3-origin.ts index c0b07281c8771..e55d1130b818f 100644 --- a/packages/aws-cdk-lib/aws-cloudfront-origins/lib/s3-origin.ts +++ b/packages/aws-cdk-lib/aws-cloudfront-origins/lib/s3-origin.ts @@ -15,7 +15,7 @@ const S3_ORIGIN_ACCESS_CONTROL_BUCKET_RESOURCE_TYPE = 'Custom::S3OriginAccessCon const BUCKET_ACTIONS: Record = { READ: ['s3:GetObject'], WRITE: ['s3:PutObject'], - DELETE: ['s3:DeleteObject'] + DELETE: ['s3:DeleteObject'], }; /** @@ -93,7 +93,10 @@ export class S3Origin implements cloudfront.IOrigin { } else if (props.originAccessIdentity) { this.origin = S3BucketOrigin.withAccessIdentity(bucket, props); } else { - this.origin = FeatureFlags.of(bucket.stack).isEnabled(cxapi.CLOUDFRONT_USE_ORIGIN_ACCESS_CONTROL_BY_DEFAULT) ? S3BucketOrigin.withAccessControl(bucket, props) : S3BucketOrigin.withAccessIdentity(bucket, props); + this.origin = FeatureFlags.of(bucket.stack) + .isEnabled(cxapi.CLOUDFRONT_USE_ORIGIN_ACCESS_CONTROL_BY_DEFAULT) ? + S3BucketOrigin.withAccessControl(bucket, props) : + S3BucketOrigin.withAccessIdentity(bucket, props); } } @@ -293,7 +296,7 @@ abstract class S3BucketOrigin extends cloudfront.OriginBase { KmsKeyId: key.keyId, AccountId: this.bucket.env.account, Partition: Stack.of(scope).partition, - AccessLevels: keyAccessLevels + AccessLevels: keyAccessLevels, }, }); } diff --git a/packages/aws-cdk-lib/aws-cloudfront-origins/test/s3-origin.test.ts b/packages/aws-cdk-lib/aws-cloudfront-origins/test/s3-origin.test.ts index f021b3a43ecca..0f78ce0f96593 100644 --- a/packages/aws-cdk-lib/aws-cloudfront-origins/test/s3-origin.test.ts +++ b/packages/aws-cdk-lib/aws-cloudfront-origins/test/s3-origin.test.ts @@ -1,7 +1,7 @@ import { Annotations, Match, Template } from '../../assertions'; import * as cloudfront from '../../aws-cloudfront'; -import * as s3 from '../../aws-s3'; import * as kms from '../../aws-kms'; +import * as s3 from '../../aws-s3'; import { App, Duration, Stack } from '../../core'; import { CLOUDFRONT_USE_ORIGIN_ACCESS_CONTROL_BY_DEFAULT } from '../../cx-api'; import { AccessLevel, S3Origin, S3OriginProps } from '../lib'; @@ -220,7 +220,7 @@ describe('With bucket', () => { originAccessIdentity: oai, originAccessControl: oac, })).toThrow('Only one of originAccessControl or originAccessIdentity can be specified for an origin.'); - }) + }); describe('with feature flag enabled', () => { beforeEach(() => { @@ -270,15 +270,14 @@ describe('With bucket', () => { }; const origin = new S3Origin(bucket, props); new cloudfront.Distribution(stack, 'Dist', { defaultBehavior: { origin } }); - console.log(Template.fromStack(stack).findResources('AWS::S3::BucketPolicy')); Template.fromStack(stack).hasResourceProperties('AWS::S3::BucketPolicy', { PolicyDocument: { Statement: [{ - Sid: "GrantOACAccessToS3", + Sid: 'GrantOACAccessToS3', Action: ['s3:GetObject', 's3:PutObject'], Effect: 'Allow', Principal: { - Service: "cloudfront.amazonaws.com", + Service: 'cloudfront.amazonaws.com', }, Resource: { 'Fn::Join': ['', [{ 'Fn::GetAtt': ['Bucket83908E77', 'Arn'] }, '/*']], @@ -286,7 +285,7 @@ describe('With bucket', () => { Condition: { StringEquals: { 'AWS:SourceArn': { - 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':cloudfront::', { Ref: 'AWS::AccountId' }, ':distribution/', { Ref: 'DistB3B78991'} ]], + 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':cloudfront::', { Ref: 'AWS::AccountId' }, ':distribution/', { Ref: 'DistB3B78991' }]], }, }, }, @@ -367,7 +366,6 @@ describe('With bucket', () => { }); }); - describe('With website-configured bucket', () => { test('renders all required properties, including custom origin config', () => { const bucket = new s3.Bucket(stack, 'Bucket', { diff --git a/packages/aws-cdk-lib/aws-cloudfront/README.md b/packages/aws-cdk-lib/aws-cloudfront/README.md index 05d5dfff872ac..7eca63af5ede0 100644 --- a/packages/aws-cdk-lib/aws-cloudfront/README.md +++ b/packages/aws-cdk-lib/aws-cloudfront/README.md @@ -38,9 +38,108 @@ new cloudfront.Distribution(this, 'myDist', { The above will treat the bucket differently based on if `IBucket.isWebsite` is set or not. If the bucket is configured as a website, the bucket is treated as an HTTP origin, and the built-in S3 redirects and error pages can be used. Otherwise, the bucket is handled as a bucket origin and -CloudFront's redirect and error handling will be used. In the latter case, the Origin will create an origin access identity and grant it access to the -underlying bucket. This can be used in conjunction with a bucket that is not public to require that your users access your content using CloudFront -URLs and not S3 URLs directly. +CloudFront's redirect and error handling will be used. + +## Restricting access to an S3 origin + +CloudFront provides two ways to send authenticated requests to an Amazon S3 origin: +origin access control (OAC) and origin access identity (OAI). +OAC is the recommended option and OAI is considered legacy +(see [Restricting access to an Amazon S3 Origin](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-restricting-access-to-s3.html)). +These can be used in conjunction with a bucket that is not public to +require that your users access your content using CloudFront URLs and not S3 URLs directly. + +> Note: OAC and OAI can only be used with an regular S3 bucket origin (not a bucket configured as a website endpoint). + +To setup origin access control for an S3 origin, you can create an `OriginAccessControl` +resource and pass it into the `originAccessControl` property of the origin: + +```ts +const myBucket = new s3.Bucket(this, 'myBucket'); +const oac = new cloudfront.OriginAccessControl(this, 'myS3OAC'); +new cloudfront.Distribution(this, 'myDist', { + defaultBehavior: { + origin: new origins.S3Origin(myBucket, { + originAccessControl: oac + }) + }, +}); +``` + +It is recommended to set the `@aws-cdk/aws-cloudfront:useOriginAccessControlByDefault` feature flag to `true`, so an OAC will be automatically created instead +of an OAI when `S3Origin` is instantiated. If you don't set this feature flag, and OAI will be created by default and granted access to the underlying bucket. + +Depending on the types of HTTP requests you need to send to the S3 origin, you can set the `AccessLevels` property to specify the level of permissions to grant CloudFront OAC. The default is read permissions only. +```ts +const myBucket = new s3.Bucket(this, 'myBucket'); +const oac = new cloudfront.OriginAccessControl(this, 'myS3OAC'); +new cloudfront.Distribution(this, 'myDist', { + defaultBehavior: { + origin: new origins.S3Origin(myBucket, { + originAccessControl: oac, + originAccessLevels: [origins.AccessLevel.READ, origins.AccessLevel.WRITE, origins.AccessLevel.DELETE] + }) + }, +}); +``` + +## Migrating from OAI to OAC + +If you are currently using OAI for your S3 origin and wish to migrate to OAC, first set the feature flag `@aws-cdk/aws-cloudfront:useOriginAccessControlByDefault` +to `true` in `cdk.json`. With this feature flag set, when you create a new `S3Origin` an Origin Access Control will be used instead of Origin Access Identity. +You can create and pass in an `OriginAccessControl` or one will be automatically created by default. Run `cdk diff` before deploying to verify the +changes to your stack. + +For more information, see [Migrating from origin access identity (OAI) to origin access control (OAC)](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-restricting-access-to-s3.html#migrate-from-oai-to-oac). + +### Using pre-existing S3 buckets + +If you are using an imported bucket for your S3 Origin and want to use OAC, first import the bucket using one of the import methods (`fromBucketName`, +`fromBucketArn` or `fromBucketAttributes`). + +To update the bucket policy to allow CloudFront access you can set the `overrideImportedBucketPolicy` property to `true`. The `S3Origin` construct +will update the S3 bucket policy by appending the following policy statement to allow CloudFront read-only access (unless otherwise specified in the `originAccessLevels` property): + +``` +{ + "Statement": { + "Sid": "GrantOACAccessToS3", + "Effect": "Allow", + "Principal": { + "Service": "cloudfront.amazonaws.com" + }, + "Action": "s3:GetObject", + "Resource": "arn:aws:s3:::/*", + "Condition": { + "StringEquals": { + "AWS:SourceArn": "arn:aws:cloudfront::111122223333:distribution/" + } + } + } +} +``` + +If your bucket previously used OAI, there will be a best-effort attempt to remove both the policy statement +that allows access to the OAI and the origin access identity itself. + +```ts +const bucket = s3.Bucket.fromBucketArn(this, 'MyExistingBucket', + 'arn:aws:s3:::mybucketname' +); + +const oac = new cloudfront.OriginAccessControl(this, 'MyOAC', { + originAccessControlOriginType: cloudfront.OriginAccessControlOriginType.S3, +}); + +const distribution = new cloudfront.Distribution(this, 'MyDistribution', { + defaultBehavior: { + origin: new origins.S3Origin(bucket, { + originAccessControl: oac, + overrideImportedBucketPolicy: true + }) + } +}); +``` #### ELBv2 Load Balancer diff --git a/packages/aws-cdk-lib/aws-cloudfront/lib/origin.ts b/packages/aws-cdk-lib/aws-cloudfront/lib/origin.ts index 5bc169dcbcbb0..58e5b9a196946 100644 --- a/packages/aws-cdk-lib/aws-cloudfront/lib/origin.ts +++ b/packages/aws-cdk-lib/aws-cloudfront/lib/origin.ts @@ -129,6 +129,7 @@ export interface OriginBindOptions { /** * The identifier of the Distribution this Origin is used for. + * @default - no distributionId */ readonly distributionId?: string | undefined; } diff --git a/packages/aws-cdk-lib/aws-cloudfront/lib/web-distribution.ts b/packages/aws-cdk-lib/aws-cloudfront/lib/web-distribution.ts index 6d512ebe4f3be..2fc726a1b15a5 100644 --- a/packages/aws-cdk-lib/aws-cloudfront/lib/web-distribution.ts +++ b/packages/aws-cdk-lib/aws-cloudfront/lib/web-distribution.ts @@ -11,7 +11,6 @@ import * as iam from '../../aws-iam'; import * as lambda from '../../aws-lambda'; import * as s3 from '../../aws-s3'; import * as cdk from '../../core'; -import { Annotations } from '../../core'; /** * HTTP status code to failover to second origin diff --git a/packages/aws-cdk-lib/cx-api/FEATURE_FLAGS.md b/packages/aws-cdk-lib/cx-api/FEATURE_FLAGS.md index 4b6e50325aee5..f20d77b8c7c01 100644 --- a/packages/aws-cdk-lib/cx-api/FEATURE_FLAGS.md +++ b/packages/aws-cdk-lib/cx-api/FEATURE_FLAGS.md @@ -72,7 +72,7 @@ Flags come in three types: | [@aws-cdk/pipelines:reduceAssetRoleTrustScope](#aws-cdkpipelinesreduceassetroletrustscope) | Remove the root account principal from PipelineAssetsFileRole trust policy | 2.141.0 | (default) | | [@aws-cdk/aws-ecs:removeDefaultDeploymentAlarm](#aws-cdkaws-ecsremovedefaultdeploymentalarm) | When enabled, remove default deployment alarm settings | 2.143.0 | (default) | | [@aws-cdk/custom-resources:logApiResponseDataPropertyTrueDefault](#aws-cdkcustom-resourceslogapiresponsedatapropertytruedefault) | When enabled, the custom resource used for `AwsCustomResource` will configure the `logApiResponseData` property as true by default | 2.145.0 | (fix) | -| [@aws-cdk/aws-cloudfront:useOriginAccessControl](#aws-cdkaws-cloudfrontuseoriginaccesscontrol) | When enabled, an origin access control will be created automatically when a new S3 origin is created. | V2NEXT | (fix) | +| [@aws-cdk/aws-cloudfront:useOriginAccessControlByDefault](#aws-cdkaws-cloudfrontuseoriginaccesscontrolbydefault) | When enabled, an origin access control will be created by default when a new S3 origin is created. | V2NEXT | (fix) | @@ -135,7 +135,7 @@ The following json shows the current recommended set of flags, as `cdk init` wou "@aws-cdk/aws-ec2:ebsDefaultGp3Volume": true, "@aws-cdk/aws-ecs:removeDefaultDeploymentAlarm": true, "@aws-cdk/custom-resources:logApiResponseDataPropertyTrueDefault": false, - "@aws-cdk/aws-cloudfront:useOriginAccessControl": true + "@aws-cdk/aws-cloudfront:useOriginAccessControlByDefault": true } } ``` @@ -1358,11 +1358,11 @@ property from the event object. | 2.145.0 | `false` | `false` | -### @aws-cdk/aws-cloudfront:useOriginAccessControl +### @aws-cdk/aws-cloudfront:useOriginAccessControlByDefault -*When enabled, an origin access control will be created automatically when a new S3 origin is created.* (fix) +*When enabled, an origin access control will be created by default when a new S3 origin is created.* (fix) -When this feature flag is enabled, an origin access control will be created automatically when a new `S3Origin` is created instead +When this feature flag is enabled, an origin access control will be created by default when a new `S3Origin` is created instead of an origin access identity (legacy). From f2d59065cd4768b40ad6697db38145e54f6b48de Mon Sep 17 00:00:00 2001 From: gracelu0 Date: Tue, 25 Jun 2024 11:46:41 -0700 Subject: [PATCH 13/14] fix feature flag README --- packages/aws-cdk-lib/cx-api/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/aws-cdk-lib/cx-api/README.md b/packages/aws-cdk-lib/cx-api/README.md index e0528747b760d..b955df5c07aa1 100644 --- a/packages/aws-cdk-lib/cx-api/README.md +++ b/packages/aws-cdk-lib/cx-api/README.md @@ -377,11 +377,11 @@ _cdk.json_ } ``` -* `@aws-cdk/aws-cloudfront:useOriginAccessControl` +* `@aws-cdk/aws-cloudfront:useOriginAccessControlByDefault` Use Origin Access Control instead of Origin Access Identity -When this feature flag is enabled, an origin access control will be created automatically when a new S3 origin is created. +When this feature flag is enabled, an origin access control will be created by default when a new S3 origin is created. _cdk.json_ @@ -389,7 +389,7 @@ _cdk.json_ ```json { "context": { - "@aws-cdk/aws-cloudfront:useOriginAccessControl": true + "@aws-cdk/aws-cloudfront:useOriginAccessControlByDefault": true } } ``` From 5e5ffd58dcc13d7fb613ba7c1eb7fdc779c3a416 Mon Sep 17 00:00:00 2001 From: gracelu0 Date: Tue, 25 Jun 2024 11:50:05 -0700 Subject: [PATCH 14/14] fix test --- .../test/integ.s3-origin-oac-ssekms.ts | 4 ++-- .../test/integ.s3-origin-oac.ts | 6 +++--- ...access-control-bucket-policy-handler.test.ts | 4 ++-- ...in-access-control-key-policy-handler.test.ts | 2 +- packages/aws-cdk-lib/cx-api/FEATURE_FLAGS.md | 17 +++++++++++++++++ 5 files changed, 25 insertions(+), 8 deletions(-) diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront-origins/test/integ.s3-origin-oac-ssekms.ts b/packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront-origins/test/integ.s3-origin-oac-ssekms.ts index 892b14ec8dd18..0ff7ec3a68c6f 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront-origins/test/integ.s3-origin-oac-ssekms.ts +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront-origins/test/integ.s3-origin-oac-ssekms.ts @@ -18,8 +18,8 @@ const originAccessControl = new cloudfront.OriginAccessControl(stack, 'OriginAcc new cloudfront.Distribution(stack, 'Distribution', { defaultBehavior: { origin: new origins.S3Origin(bucket, { - originAccessControl: originAccessControl - }) + originAccessControl: originAccessControl, + }), }, }); diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront-origins/test/integ.s3-origin-oac.ts b/packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront-origins/test/integ.s3-origin-oac.ts index c8e3ecb3bf378..57e85f793a25a 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront-origins/test/integ.s3-origin-oac.ts +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-cloudfront-origins/test/integ.s3-origin-oac.ts @@ -11,10 +11,10 @@ const stack = new cdk.Stack(app, 'cloudfront-s3-origin-oac'); const bucket = new s3.Bucket(stack, 'Bucket'); const originAccessControl = new cloudfront.OriginAccessControl(stack, 'OriginAccessControl'); new cloudfront.Distribution(stack, 'Distribution', { - defaultBehavior: { + defaultBehavior: { origin: new origins.S3Origin(bucket, { - originAccessControl: originAccessControl - }) + originAccessControl: originAccessControl, + }), }, }); diff --git a/packages/@aws-cdk/custom-resource-handlers/test/aws-cloudfront-origins/s3-origin-access-control-bucket-policy-handler.test.ts b/packages/@aws-cdk/custom-resource-handlers/test/aws-cloudfront-origins/s3-origin-access-control-bucket-policy-handler.test.ts index 1ab584939d679..079d3daa36a1e 100644 --- a/packages/@aws-cdk/custom-resource-handlers/test/aws-cloudfront-origins/s3-origin-access-control-bucket-policy-handler.test.ts +++ b/packages/@aws-cdk/custom-resource-handlers/test/aws-cloudfront-origins/s3-origin-access-control-bucket-policy-handler.test.ts @@ -46,12 +46,12 @@ describe('S3 OAC bucket policy handler', () => { }) }); -describe('updatePolicy', () => { +describe('appendStatementToPolicy', () => { it('should add a new policy statement if it does not exist', () => { const currentPolicy = { Statement: [] }; const policyStatementToAdd = { Sid: 'NewStatement', Effect: 'Allow', Action: 's3:GetObject', Resource: 'arn:aws:s3:::bucket/*' }; - const updatedPolicy = updatePolicy(currentPolicy, policyStatementToAdd); + const updatedPolicy = appendStatementToPolicy(currentPolicy, policyStatementToAdd); expect(updatedPolicy.Statement).toContainEqual(policyStatementToAdd); }); diff --git a/packages/@aws-cdk/custom-resource-handlers/test/aws-cloudfront-origins/s3-origin-access-control-key-policy-handler.test.ts b/packages/@aws-cdk/custom-resource-handlers/test/aws-cloudfront-origins/s3-origin-access-control-key-policy-handler.test.ts index e69c7b5b2e02e..028ae63c46f4e 100644 --- a/packages/@aws-cdk/custom-resource-handlers/test/aws-cloudfront-origins/s3-origin-access-control-key-policy-handler.test.ts +++ b/packages/@aws-cdk/custom-resource-handlers/test/aws-cloudfront-origins/s3-origin-access-control-key-policy-handler.test.ts @@ -149,7 +149,7 @@ describe('getActions', () => { }); }); -describe('updatePolicy', () => { +describe('appendStatementToPolicy', () => { it('should add a new policy statement if it does not exist', () => { const currentPolicy = { Statement: [] }; const policyStatementToAdd = { Sid: 'NewStatement', Effect: 'Allow', Action: ['kms:Decrypt'], Resource: '*' }; diff --git a/packages/aws-cdk-lib/cx-api/FEATURE_FLAGS.md b/packages/aws-cdk-lib/cx-api/FEATURE_FLAGS.md index f20d77b8c7c01..408ce1ac4ec65 100644 --- a/packages/aws-cdk-lib/cx-api/FEATURE_FLAGS.md +++ b/packages/aws-cdk-lib/cx-api/FEATURE_FLAGS.md @@ -73,6 +73,7 @@ Flags come in three types: | [@aws-cdk/aws-ecs:removeDefaultDeploymentAlarm](#aws-cdkaws-ecsremovedefaultdeploymentalarm) | When enabled, remove default deployment alarm settings | 2.143.0 | (default) | | [@aws-cdk/custom-resources:logApiResponseDataPropertyTrueDefault](#aws-cdkcustom-resourceslogapiresponsedatapropertytruedefault) | When enabled, the custom resource used for `AwsCustomResource` will configure the `logApiResponseData` property as true by default | 2.145.0 | (fix) | | [@aws-cdk/aws-cloudfront:useOriginAccessControlByDefault](#aws-cdkaws-cloudfrontuseoriginaccesscontrolbydefault) | When enabled, an origin access control will be created by default when a new S3 origin is created. | V2NEXT | (fix) | +| [@aws-cdk/aws-stepfunctions-tasks:ecsReduceRunTaskPermissions](#aws-cdkaws-stepfunctions-tasksecsreduceruntaskpermissions) | When enabled, IAM Policy created to run tasks won't include the task definition ARN, only the revision ARN. | V2NEXT | (fix) | @@ -135,6 +136,7 @@ The following json shows the current recommended set of flags, as `cdk init` wou "@aws-cdk/aws-ec2:ebsDefaultGp3Volume": true, "@aws-cdk/aws-ecs:removeDefaultDeploymentAlarm": true, "@aws-cdk/custom-resources:logApiResponseDataPropertyTrueDefault": false, + "@aws-cdk/aws-stepfunctions-tasks:ecsReduceRunTaskPermissions": true, "@aws-cdk/aws-cloudfront:useOriginAccessControlByDefault": true } } @@ -1372,4 +1374,19 @@ of an origin access identity (legacy). | V2NEXT | `false` | `true` | +### @aws-cdk/aws-stepfunctions-tasks:ecsReduceRunTaskPermissions + +*When enabled, IAM Policy created to run tasks won't include the task definition ARN, only the revision ARN.* (fix) + +When this feature flag is enabled, the IAM Policy created to run tasks won't include the task definition ARN, only the revision ARN. +The revision ARN is more specific than the task definition ARN. See https://docs.aws.amazon.com/step-functions/latest/dg/ecs-iam.html +for more details. + + +| Since | Default | Recommended | +| ----- | ----- | ----- | +| (not in v1) | | | +| V2NEXT | `false` | `true` | + +