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..0ff7ec3a68c6f --- /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 new file mode 100644 index 0000000000000..57e85f793a25a --- /dev/null +++ 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 new file mode 100644 index 0000000000000..fc25c1eeda0a4 --- /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,187 @@ +/* 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_OAC_POLICY_SID = 'GrantOACAccessToS3'; +const s3 = new S3({}); + +interface updateBucketPolicyProps { + bucketName: string; + distributionId: string; + partition: string; + accountId: string; + actions: string[]; +} + +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') { + await updateBucketPolicy({ bucketName, distributionId, partition, accountId, actions }); + + return { + IsComplete: true, + }; + } else if (event.RequestType === 'Delete') { + await removeOacPolicyStatement( + bucketName, + distributionId, + partition, + accountId + ) + return { + IsComplete: true, + }; + } else { + return; + } +} + +export 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: S3_OAC_POLICY_SID, + Principal: { + Service: ['cloudfront.amazonaws.com'], + }, + Effect: 'Allow', + Action: props.actions, + 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 = appendStatementToPolicy(policy, oacBucketPolicyStatement); + console.log('Updated bucket policy', JSON.stringify(updatedBucketPolicy, undefined, 2)); + + await s3.putBucketPolicy({ + Bucket: props.bucketName, + Policy: JSON.stringify(updatedBucketPolicy), + }); + + // Check if policy has OAI principal and remove + await removeOaiPolicyStatements(updatedBucketPolicy, props.bucketName); + + } 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, 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. + */ +export function appendStatementToPolicy(currentPolicy: any, policyStatementToAdd: any) { + if (!isStatementInPolicy(currentPolicy, policyStatementToAdd)) { + currentPolicy.Statement.push(policyStatementToAdd); + } + + // Return the result + return currentPolicy; +}; + +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 + */ +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')) { + return true; + } + } + return false; +} + +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 new file mode 100644 index 0000000000000..ebbbd3ba7f692 --- /dev/null +++ b/packages/@aws-cdk/custom-resource-handlers/lib/aws-cloudfront-origins/s3-origin-access-control-key-policy-handler/index.ts @@ -0,0 +1,142 @@ +/* eslint-disable no-console */ +/* eslint-disable import/no-extraneous-dependencies */ +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) { + 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 describeKeyCommandResponse = await kms.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 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 actions = getActions(accessLevels) + + // Define the updated key policy to allow CloudFront Distribution access + const kmsKeyPolicyStatement = { + Sid: 'GrantOACAccessToKMS', + Effect: 'Allow', + Principal: { + Service: [ + 'cloudfront.amazonaws.com', + ], + }, + Action: actions, + Resource: `arn:${partition}:kms:${region}:${accountId}:key/${kmsKeyId}`, + Condition: { + StringEquals: { + 'AWS:SourceArn': `arn:${partition}:cloudfront::${accountId}:distribution/${distributionId}`, + }, + }, + }; + + const updatedKeyPolicy = appendStatementToPolicy(keyPolicy, kmsKeyPolicyStatement); + console.log('Updated key policy', JSON.stringify(updatedKeyPolicy, undefined, 2)); + await kms.putKeyPolicy({ + KeyId: kmsKeyId, + Policy: JSON.stringify(updatedKeyPolicy), + PolicyName: 'default', + }); + + return { + IsComplete: true, + }; + } 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, + }; + } else { + return; + } +} + +export 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 + * 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 function appendStatementToPolicy(currentPolicy: any, policyStatementToAdd: any) { + if (!isStatementInPolicy(currentPolicy, policyStatementToAdd)) { + currentPolicy.Statement.push(policyStatementToAdd); + } + // Return the result + return currentPolicy; +}; + +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 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..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 @@ -118,6 +118,20 @@ export const config: HandlerFrameworkConfig = { }, ], }, + 'aws-cloudfront-origins': { + 's3-origin-access-control-key-policy-provider': [ + { + type: ComponentType.CUSTOM_RESOURCE_PROVIDER, + 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'), + }, + ], + }, '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/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..079d3daa36a1e --- /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,235 @@ +import { S3Client, GetBucketPolicyCommand, PutBucketPolicyCommand } from '@aws-sdk/client-s3'; +import { mockClient } from 'aws-sdk-client-mock'; +import 'aws-sdk-client-mock-jest'; +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); + +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('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 = appendStatementToPolicy(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 = appendStatementToPolicy(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 new file mode 100644 index 0000000000000..028ae63c46f4e --- /dev/null +++ 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, appendStatementToPolicy, 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('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: '*' }; + const updatedPolicy = appendStatementToPolicy(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 = appendStatementToPolicy(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/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 e57a91b110c27..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 @@ -2,8 +2,21 @@ 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 * as cdk 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'; + +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. @@ -15,6 +28,44 @@ 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; + + /** + * 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. This property only applies to OAC (not OAI). + * @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', } /** @@ -28,12 +79,25 @@ 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.originAccessControl) { + this.origin = S3BucketOrigin.withAccessControl(bucket, props); + } 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); + } } public bind(scope: Construct, options: cloudfront.OriginBindOptions): cloudfront.OriginBindConfig { @@ -44,46 +108,210 @@ 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 identities. + * Contains additional logic around bucket permissions and origin access control (via OAI or OAC). */ -class S3BucketOrigin extends cloudfront.OriginBase { - private originAccessIdentity!: cloudfront.IOriginAccessIdentity; +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; - constructor(private readonly bucket: s3.IBucket, { originAccessIdentity, ...props }: S3OriginProps) { - super(bucket.bucketRegionalDomainName, props); - if (originAccessIdentity) { - this.originAccessIdentity = originAccessIdentity; - } + 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.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 = cdk.Stack.of(this.bucket); - const bucketInDifferentStack = bucketStack !== cdk.Stack.of(scope); - const oaiScope = bucketInDifferentStack ? bucketStack : scope; - const oaiId = bucketInDifferentStack ? `${cdk.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 super.bind(scope, options); + 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'); + } + + 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; + 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, 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.'); + } + } + + if (this.bucket.encryptionKey) { + this.grantDistributionAccessToKey( + scope, + distributionId, + this.bucket.encryptionKey, + props.originAccessLevels ?? [AccessLevel.READ], + ); + } + + const originBindConfig = this._bind(scope, options); + + // Update configuration to set OriginControlAccessId property + return { + ...originBindConfig, + originProperty: { + ...originBindConfig.originProperty!, + originAccessControlId: this.originAccessControl.originAccessControlId, + }, + }; + } + + /** + * 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 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: 'GrantOACAccessToS3', + effect: iam.Effect.ALLOW, + principals: [new iam.ServicePrincipal('cloudfront.amazonaws.com')], + actions, + 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 | 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.', + }); + 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, + Actions: actions, + }, + }); + } + + /** + * Use custom resource to update KMS key policy + */ + 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.', + }); + provider.addToRolePolicy({ + Action: ['kms:PutKeyPolicy', 'kms:GetKeyPolicy', 'kms:DescribeKey'], + Effect: 'Allow', + 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, + properties: { + DistributionId: distributionId, + KmsKeyId: key.keyId, + AccountId: this.bucket.env.account, + Partition: Stack.of(scope).partition, + AccessLevels: keyAccessLevels, + }, + }); + } + }); + } + + protected constructor(protected readonly bucket: s3.IBucket, props: S3OriginProps = {}) { + super(bucket.bucketRegionalDomainName, props); } - protected renderS3OriginConfig(): cloudfront.CfnDistribution.S3OriginConfigProperty | undefined { - return { originAccessIdentity: `origin-access-identity/cloudfront/${this.originAccessIdentity.originAccessIdentityId}` }; + public abstract bind(scope: Construct, options: cloudfront.OriginBindOptions): cloudfront.OriginBindConfig; + + protected abstract renderS3OriginConfig(): cloudfront.CfnDistribution.S3OriginConfigProperty | undefined; + + protected _bind(scope: Construct, options: cloudfront.OriginBindOptions): cloudfront.OriginBindConfig { + return super.bind(scope, options); } } 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..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,8 +1,10 @@ -import { Match, Template } from '../../assertions'; +import { Annotations, Match, Template } from '../../assertions'; import * as cloudfront from '../../aws-cloudfront'; +import * as kms from '../../aws-kms'; import * as s3 from '../../aws-s3'; 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,6 +210,160 @@ 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 } }); + 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', () => { diff --git a/packages/aws-cdk-lib/aws-cloudfront/README.md b/packages/aws-cdk-lib/aws-cloudfront/README.md index 965e3cbac7f69..7eca63af5ede0 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. @@ -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 @@ -951,7 +1050,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/distribution.ts b/packages/aws-cdk-lib/aws-cloudfront/lib/distribution.ts index 336affec8b862..e287cb7d60c15 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: 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) { 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..9fc74e1c81629 --- /dev/null +++ b/packages/aws-cdk-lib/aws-cloudfront/lib/origin-access-control.ts @@ -0,0 +1,170 @@ +import { Construct } from 'constructs'; +import { CfnOriginAccessControl } from './cloudfront.generated'; +import { IResource, Resource, Names, Token } from '../../core'; + +/** + * Represents a CloudFront Origin Access Control + */ +export interface IOriginAccessControl extends IResource { + /** + * The unique identifier of the origin access control. + * @attribute + */ + readonly originAccessControlId: string; + + /** + * The type of origin that the origin access control is for. + * @attribute + */ + readonly originAccessControlOriginType: string; +} + +/** + * Properties for creating a Origin Access Control 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 OriginAccessControlOriginType.S3 + */ + readonly originAccessControlOriginType?: OriginAccessControlOriginType; + /** + * Specifies which requests CloudFront signs. + * @default SigningBehavior.ALWAYS + */ + readonly signingBehavior?: SigningBehavior; + /** + * The signing protocol of the origin access control. + * @default SigningProtocol.SIGV4 + */ + readonly signingProtocol?: SigningProtocol; +} + +/** + * Attributes for a CloudFront Origin Access Control + */ +export interface OriginAccessControlAttributes { + /** + * The unique identifier of the origin access control. + */ + readonly originAccessControlId: string; + + /** + * The type of origin that the origin access control is for. + */ + readonly originAccessControlOriginType: string; +} + +/** + * Origin types supported by Origin Access Control. + */ +export enum OriginAccessControlOriginType { + /** + * Uses an Amazon S3 bucket origin. + */ + S3 = 's3', +} + +/** + * 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 Resource implements IOriginAccessControl { + /** + * Imports an origin access control from its id and origin type. + */ + public static fromOriginAccessControlAttributes(scope: Construct, id: string, attrs: OriginAccessControlAttributes): IOriginAccessControl { + class Import extends Resource implements IOriginAccessControl { + public readonly originAccessControlId = attrs.originAccessControlId; + public readonly originAccessControlOriginType = attrs.originAccessControlOriginType; + } + return new Import(scope, id); + } + + /** + * 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; + + // 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, + name: props.originAccessControlName ?? Names.uniqueResourceName(this, { + maxLength: 64, + }), + signingBehavior: props.signingBehavior ?? SigningBehavior.ALWAYS, + signingProtocol: props.signingProtocol ?? SigningProtocol.SIGV4, + originAccessControlOriginType: this.originAccessControlOriginType, + }, + }); + + 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 2044cbc5fe489..58e5b9a196946 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,12 @@ 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. + * @default - no distributionId + */ + readonly distributionId?: string | undefined; } /** @@ -134,6 +147,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 +162,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 +187,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..2fc726a1b15a5 100644 --- a/packages/aws-cdk-lib/aws-cloudfront/lib/web-distribution.ts +++ b/packages/aws-cdk-lib/aws-cloudfront/lib/web-distribution.ts @@ -741,6 +741,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 { 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..a89465be6b508 --- /dev/null +++ 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 e67806c3ceca1..408ce1ac4ec65 100644 --- a/packages/aws-cdk-lib/cx-api/FEATURE_FLAGS.md +++ b/packages/aws-cdk-lib/cx-api/FEATURE_FLAGS.md @@ -72,6 +72,8 @@ 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: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) | @@ -133,7 +135,9 @@ The following json shows the current recommended set of flags, as `cdk init` wou "@aws-cdk/aws-eks:nodegroupNameAttribute": true, "@aws-cdk/aws-ec2:ebsDefaultGp3Volume": true, "@aws-cdk/aws-ecs:removeDefaultDeploymentAlarm": true, - "@aws-cdk/custom-resources:logApiResponseDataPropertyTrueDefault": false + "@aws-cdk/custom-resources:logApiResponseDataPropertyTrueDefault": false, + "@aws-cdk/aws-stepfunctions-tasks:ecsReduceRunTaskPermissions": true, + "@aws-cdk/aws-cloudfront:useOriginAccessControlByDefault": true } } ``` @@ -1294,7 +1298,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 | @@ -1356,4 +1360,33 @@ property from the event object. | 2.145.0 | `false` | `false` | +### @aws-cdk/aws-cloudfront:useOriginAccessControlByDefault + +*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 by default when a new `S3Origin` is created instead +of an origin access identity (legacy). + + +| Since | Default | Recommended | +| ----- | ----- | ----- | +| (not in v1) | | | +| 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` | + + diff --git a/packages/aws-cdk-lib/cx-api/README.md b/packages/aws-cdk-lib/cx-api/README.md index 2aca6f69abaf9..b955df5c07aa1 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:useOriginAccessControlByDefault` + +Use Origin Access Control instead of Origin Access Identity + +When this feature flag is enabled, an origin access control will be created by default when a new S3 origin is created. + + +_cdk.json_ + +```json +{ + "context": { + "@aws-cdk/aws-cloudfront:useOriginAccessControlByDefault": 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..1e9439ed91701 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_BY_DEFAULT = '@aws-cdk/aws-cloudfront:useOriginAccessControlByDefault'; 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_BY_DEFAULT]: { + type: FlagType.BugFix, + 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 by default when a new \`S3Origin\` is created instead + of an origin access identity (legacy). + `, + introducedIn: { v2: 'V2NEXT' }, + recommendedValue: true, + }, }; const CURRENT_MV = 'v2';