diff --git a/packages/@aws-cdk-testing/cli-integ/lib/eventually.ts b/packages/@aws-cdk-testing/cli-integ/lib/eventually.ts new file mode 100644 index 0000000000000..6936ff32f76fd --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/lib/eventually.ts @@ -0,0 +1,42 @@ +/** + * @param maxAttempts the maximum number of attempts + * @param interval interval in milliseconds to observe between attempts + */ +export type EventuallyOptions = { + maxAttempts?: number; + interval?: number; +}; + +const wait = (ms: number): Promise => new Promise((resolve) => setTimeout(resolve, ms)); +const DEFAULT_INTERVAL = 1000; +const DEFAULT_MAX_ATTEMPTS = 10; + +/** + * Runs a function on an interval until the maximum number of attempts has + * been reached. + * + * Default interval = 1000 milliseconds + * Default maxAttempts = 10 + * + * @param fn function to run + * @param options EventuallyOptions + */ +const eventually = async (call: () => Promise, options?: EventuallyOptions): Promise => { + const opts = { + interval: options?.interval ? options.interval : DEFAULT_INTERVAL, + maxAttempts: options?.maxAttempts ? options.maxAttempts : DEFAULT_MAX_ATTEMPTS, + }; + + while (opts.maxAttempts-- >= 0) { + try { + return await call(); + } catch (err) { + if (opts.maxAttempts <= 0) throw err; + } + await wait(opts.interval); + } + + throw new Error('An unexpected error has occurred.'); +}; + +export default eventually; \ No newline at end of file diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/bootstrapping.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/bootstrapping.integtest.ts index 18889bde7dd7e..ab56611bf39e0 100644 --- a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/bootstrapping.integtest.ts +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/bootstrapping.integtest.ts @@ -3,6 +3,7 @@ import * as fs from 'fs'; import * as path from 'path'; import * as yaml from 'yaml'; import { integTest, randomString, withoutBootstrap } from '../../lib'; +import eventually from '../../lib/eventually'; jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime @@ -283,17 +284,25 @@ integTest('can remove customPermissionsBoundary', withoutBootstrap(async (fixtur }), }); policyArn = policy.Policy?.Arn; - await fixture.cdkBootstrapModern({ - // toolkitStackName doesn't matter for this particular invocation - toolkitStackName: bootstrapStackName, - customPermissionsBoundary: policyName, - }); - const response = await fixture.aws.cloudFormation('describeStacks', { StackName: bootstrapStackName }); - expect( - response.Stacks?.[0].Parameters?.some( - param => (param.ParameterKey === 'InputPermissionsBoundary' && param.ParameterValue === policyName), - )).toEqual(true); + // Policy creation and consistency across regions is "almost immediate" + // See: https://docs.aws.amazon.com/IAM/latest/UserGuide/troubleshoot_general.html#troubleshoot_general_eventual-consistency + // We will put this in an `eventually` block to retry stack creation with a reasonable timeout + const createStackWithPermissionBoundary = async (): Promise => { + await fixture.cdkBootstrapModern({ + // toolkitStackName doesn't matter for this particular invocation + toolkitStackName: bootstrapStackName, + customPermissionsBoundary: policyName, + }); + + const response = await fixture.aws.cloudFormation('describeStacks', { StackName: bootstrapStackName }); + expect( + response.Stacks?.[0].Parameters?.some( + param => (param.ParameterKey === 'InputPermissionsBoundary' && param.ParameterValue === policyName), + )).toEqual(true); + }; + + await eventually(createStackWithPermissionBoundary, { maxAttempts: 3 }); await fixture.cdkBootstrapModern({ // toolkitStackName doesn't matter for this particular invocation diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-ses-actions/test/integ.actions.js.snapshot/asset.96d0b6be9a64ae309bf89a86f5515453f0fa1d07b4f6b37198051cc98e251f34/index.d.ts b/packages/@aws-cdk-testing/framework-integ/test/aws-ses-actions/test/integ.actions.js.snapshot/asset.96d0b6be9a64ae309bf89a86f5515453f0fa1d07b4f6b37198051cc98e251f34/index.d.ts deleted file mode 100644 index 2cd579eef56f1..0000000000000 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-ses-actions/test/integ.actions.js.snapshot/asset.96d0b6be9a64ae309bf89a86f5515453f0fa1d07b4f6b37198051cc98e251f34/index.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -export declare function handler(event: AWSLambda.SESEvent): Promise<{ - disposition: string; -} | null>; diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-ses-actions/test/integ.actions.js.snapshot/asset.96d0b6be9a64ae309bf89a86f5515453f0fa1d07b4f6b37198051cc98e251f34/index.js b/packages/@aws-cdk-testing/framework-integ/test/aws-ses-actions/test/integ.actions.js.snapshot/asset.96d0b6be9a64ae309bf89a86f5515453f0fa1d07b4f6b37198051cc98e251f34/index.js deleted file mode 100644 index c633477efc64d..0000000000000 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-ses-actions/test/integ.actions.js.snapshot/asset.96d0b6be9a64ae309bf89a86f5515453f0fa1d07b4f6b37198051cc98e251f34/index.js +++ /dev/null @@ -1,22 +0,0 @@ -"use strict"; -/* eslint-disable no-console */ -Object.defineProperty(exports, "__esModule", { value: true }); -exports.handler = void 0; -// Adapted from https://docs.aws.amazon.com/ses/latest/DeveloperGuide/receiving-email-action-lambda-example-functions.html -async function handler(event) { - console.log('Spam filter'); - const sesNotification = event.Records[0].ses; - console.log('SES Notification: %j', sesNotification); - // Check if any spam check failed - if (sesNotification.receipt.spfVerdict.status === 'FAIL' - || sesNotification.receipt.dkimVerdict.status === 'FAIL' - || sesNotification.receipt.spamVerdict.status === 'FAIL' - || sesNotification.receipt.virusVerdict.status === 'FAIL') { - console.log('Dropping spam'); - // Stop processing rule set, dropping message - return { disposition: 'STOP_RULE_SET' }; - } - return null; -} -exports.handler = handler; -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJpbmRleC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiO0FBQUEsK0JBQStCOzs7QUFFL0IsMEhBQTBIO0FBQ25ILEtBQUssVUFBVSxPQUFPLENBQUMsS0FBeUI7SUFDckQsT0FBTyxDQUFDLEdBQUcsQ0FBQyxhQUFhLENBQUMsQ0FBQztJQUUzQixNQUFNLGVBQWUsR0FBRyxLQUFLLENBQUMsT0FBTyxDQUFDLENBQUMsQ0FBQyxDQUFDLEdBQUcsQ0FBQztJQUM3QyxPQUFPLENBQUMsR0FBRyxDQUFDLHNCQUFzQixFQUFFLGVBQWUsQ0FBQyxDQUFDO0lBRXJELGlDQUFpQztJQUNqQyxJQUFJLGVBQWUsQ0FBQyxPQUFPLENBQUMsVUFBVSxDQUFDLE1BQU0sS0FBSyxNQUFNO1dBQ2pELGVBQWUsQ0FBQyxPQUFPLENBQUMsV0FBVyxDQUFDLE1BQU0sS0FBSyxNQUFNO1dBQ3JELGVBQWUsQ0FBQyxPQUFPLENBQUMsV0FBVyxDQUFDLE1BQU0sS0FBSyxNQUFNO1dBQ3JELGVBQWUsQ0FBQyxPQUFPLENBQUMsWUFBWSxDQUFDLE1BQU0sS0FBSyxNQUFNLEVBQUU7UUFDN0QsT0FBTyxDQUFDLEdBQUcsQ0FBQyxlQUFlLENBQUMsQ0FBQztRQUU3Qiw2Q0FBNkM7UUFDN0MsT0FBTyxFQUFFLFdBQVcsRUFBRSxlQUFlLEVBQUUsQ0FBQztLQUN6QztJQUVELE9BQU8sSUFBSSxDQUFDO0FBQ2QsQ0FBQztBQWxCRCwwQkFrQkMiLCJzb3VyY2VzQ29udGVudCI6WyIvKiBlc2xpbnQtZGlzYWJsZSBuby1jb25zb2xlICovXG5cbi8vIEFkYXB0ZWQgZnJvbSBodHRwczovL2RvY3MuYXdzLmFtYXpvbi5jb20vc2VzL2xhdGVzdC9EZXZlbG9wZXJHdWlkZS9yZWNlaXZpbmctZW1haWwtYWN0aW9uLWxhbWJkYS1leGFtcGxlLWZ1bmN0aW9ucy5odG1sXG5leHBvcnQgYXN5bmMgZnVuY3Rpb24gaGFuZGxlcihldmVudDogQVdTTGFtYmRhLlNFU0V2ZW50KTogUHJvbWlzZTx7IGRpc3Bvc2l0aW9uOiBzdHJpbmcgfSB8IG51bGw+IHtcbiAgY29uc29sZS5sb2coJ1NwYW0gZmlsdGVyJyk7XG5cbiAgY29uc3Qgc2VzTm90aWZpY2F0aW9uID0gZXZlbnQuUmVjb3Jkc1swXS5zZXM7XG4gIGNvbnNvbGUubG9nKCdTRVMgTm90aWZpY2F0aW9uOiAlaicsIHNlc05vdGlmaWNhdGlvbik7XG5cbiAgLy8gQ2hlY2sgaWYgYW55IHNwYW0gY2hlY2sgZmFpbGVkXG4gIGlmIChzZXNOb3RpZmljYXRpb24ucmVjZWlwdC5zcGZWZXJkaWN0LnN0YXR1cyA9PT0gJ0ZBSUwnXG4gICAgICB8fCBzZXNOb3RpZmljYXRpb24ucmVjZWlwdC5ka2ltVmVyZGljdC5zdGF0dXMgPT09ICdGQUlMJ1xuICAgICAgfHwgc2VzTm90aWZpY2F0aW9uLnJlY2VpcHQuc3BhbVZlcmRpY3Quc3RhdHVzID09PSAnRkFJTCdcbiAgICAgIHx8IHNlc05vdGlmaWNhdGlvbi5yZWNlaXB0LnZpcnVzVmVyZGljdC5zdGF0dXMgPT09ICdGQUlMJykge1xuICAgIGNvbnNvbGUubG9nKCdEcm9wcGluZyBzcGFtJyk7XG5cbiAgICAvLyBTdG9wIHByb2Nlc3NpbmcgcnVsZSBzZXQsIGRyb3BwaW5nIG1lc3NhZ2VcbiAgICByZXR1cm4geyBkaXNwb3NpdGlvbjogJ1NUT1BfUlVMRV9TRVQnIH07XG4gIH1cblxuICByZXR1cm4gbnVsbDtcbn1cbiJdfQ== \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-ses-actions/test/integ.actions.js.snapshot/asset.96d0b6be9a64ae309bf89a86f5515453f0fa1d07b4f6b37198051cc98e251f34/index.ts b/packages/@aws-cdk-testing/framework-integ/test/aws-ses-actions/test/integ.actions.js.snapshot/asset.96d0b6be9a64ae309bf89a86f5515453f0fa1d07b4f6b37198051cc98e251f34/index.ts deleted file mode 100644 index 76a639acdf50e..0000000000000 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-ses-actions/test/integ.actions.js.snapshot/asset.96d0b6be9a64ae309bf89a86f5515453f0fa1d07b4f6b37198051cc98e251f34/index.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* eslint-disable no-console */ - -// Adapted from https://docs.aws.amazon.com/ses/latest/DeveloperGuide/receiving-email-action-lambda-example-functions.html -export async function handler(event: AWSLambda.SESEvent): Promise<{ disposition: string } | null> { - console.log('Spam filter'); - - const sesNotification = event.Records[0].ses; - console.log('SES Notification: %j', sesNotification); - - // Check if any spam check failed - if (sesNotification.receipt.spfVerdict.status === 'FAIL' - || sesNotification.receipt.dkimVerdict.status === 'FAIL' - || sesNotification.receipt.spamVerdict.status === 'FAIL' - || sesNotification.receipt.virusVerdict.status === 'FAIL') { - console.log('Dropping spam'); - - // Stop processing rule set, dropping message - return { disposition: 'STOP_RULE_SET' }; - } - - return null; -} diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-ses-actions/test/integ.actions.js.snapshot/aws-cdk-ses-receipt.assets.json b/packages/@aws-cdk-testing/framework-integ/test/aws-ses-actions/test/integ.actions.js.snapshot/aws-cdk-ses-receipt.assets.json index d6156ec124239..35cbc31e26575 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-ses-actions/test/integ.actions.js.snapshot/aws-cdk-ses-receipt.assets.json +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-ses-actions/test/integ.actions.js.snapshot/aws-cdk-ses-receipt.assets.json @@ -1,20 +1,7 @@ { - "version": "34.0.0", + "version": "36.0.0", "files": { - "96d0b6be9a64ae309bf89a86f5515453f0fa1d07b4f6b37198051cc98e251f34": { - "source": { - "path": "asset.96d0b6be9a64ae309bf89a86f5515453f0fa1d07b4f6b37198051cc98e251f34", - "packaging": "zip" - }, - "destinations": { - "current_account-current_region": { - "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", - "objectKey": "96d0b6be9a64ae309bf89a86f5515453f0fa1d07b4f6b37198051cc98e251f34.zip", - "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" - } - } - }, - "ea7e677fcdb536a575f56f02244366de338d8c370dbf941f7020229798da8db7": { + "558c1448bff6417f14bd77ab3ed5fda1c051566e3e95f20865a1b5d5ac53d66c": { "source": { "path": "aws-cdk-ses-receipt.template.json", "packaging": "file" @@ -22,7 +9,7 @@ "destinations": { "current_account-current_region": { "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", - "objectKey": "ea7e677fcdb536a575f56f02244366de338d8c370dbf941f7020229798da8db7.json", + "objectKey": "558c1448bff6417f14bd77ab3ed5fda1c051566e3e95f20865a1b5d5ac53d66c.json", "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" } } diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-ses-actions/test/integ.actions.js.snapshot/aws-cdk-ses-receipt.template.json b/packages/@aws-cdk-testing/framework-integ/test/aws-ses-actions/test/integ.actions.js.snapshot/aws-cdk-ses-receipt.template.json index 9fbda65cd40b4..5363ad5b9d605 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-ses-actions/test/integ.actions.js.snapshot/aws-cdk-ses-receipt.template.json +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-ses-actions/test/integ.actions.js.snapshot/aws-cdk-ses-receipt.template.json @@ -86,8 +86,31 @@ "Action": "s3:PutObject", "Condition": { "StringEquals": { - "aws:Referer": { + "aws:SourceAccount": { "Ref": "AWS::AccountId" + }, + "aws:SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ses:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":receipt-rule-set/INBOUND_MAIL:receipt-rule/", + { + "Ref": "RuleSetFirstRule0A27C8CC" + } + ] + ] } } }, @@ -172,40 +195,9 @@ "UpdateReplacePolicy": "Retain", "DeletionPolicy": "Retain" }, - "RuleSetE30C6C48": { - "Type": "AWS::SES::ReceiptRuleSet" - }, - "RuleSetDropSpamRule5809F51B": { - "Type": "AWS::SES::ReceiptRule", - "Properties": { - "Rule": { - "Actions": [ - { - "LambdaAction": { - "FunctionArn": { - "Fn::GetAtt": [ - "SingletonLambda224e77f9a32e4b4dac32983477abba164533EA15", - "Arn" - ] - }, - "InvocationType": "RequestResponse" - } - } - ], - "Enabled": true, - "ScanEnabled": true - }, - "RuleSetName": { - "Ref": "RuleSetE30C6C48" - } - } - }, "RuleSetFirstRule0A27C8CC": { "Type": "AWS::SES::ReceiptRule", "Properties": { - "After": { - "Ref": "RuleSetDropSpamRule5809F51B" - }, "Rule": { "Actions": [ { @@ -256,7 +248,7 @@ { "BounceAction": { "Message": "Message content rejected", - "Sender": "cdk-ses-receipt-test@yopmail.com", + "Sender": "test@cdk-test-123.awsapps.com", "SmtpReplyCode": "500", "StatusCode": "5.6.1", "TopicArn": { @@ -268,17 +260,14 @@ "Enabled": true, "Name": "FirstRule", "Recipients": [ - "cdk-ses-receipt-test@yopmail.com" + "test@cdk-test-123.awsapps.com" ], "ScanEnabled": true, "TlsPolicy": "Require" }, - "RuleSetName": { - "Ref": "RuleSetE30C6C48" - } + "RuleSetName": "INBOUND_MAIL" }, "DependsOn": [ - "BucketPolicyE9A3008A", "FunctionAllowSes1829904A" ] }, @@ -301,79 +290,66 @@ ], "Enabled": true }, - "RuleSetName": { - "Ref": "RuleSetE30C6C48" - } + "RuleSetName": "INBOUND_MAIL" } }, - "SingletonLambda224e77f9a32e4b4dac32983477abba16ServiceRole3037F5B4": { - "Type": "AWS::IAM::Role", + "NotificationQueue36610CC1": { + "Type": "AWS::SQS::Queue", + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "NotificationQueuePolicyCC060EA6": { + "Type": "AWS::SQS::QueuePolicy", "Properties": { - "AssumeRolePolicyDocument": { + "PolicyDocument": { "Statement": [ { - "Action": "sts:AssumeRole", + "Action": "sqs:SendMessage", + "Condition": { + "ArnEquals": { + "aws:SourceArn": { + "Ref": "TopicBFC7AF6E" + } + } + }, "Effect": "Allow", "Principal": { - "Service": "lambda.amazonaws.com" + "Service": "sns.amazonaws.com" + }, + "Resource": { + "Fn::GetAtt": [ + "NotificationQueue36610CC1", + "Arn" + ] } } ], "Version": "2012-10-17" }, - "ManagedPolicyArns": [ + "Queues": [ { - "Fn::Join": [ - "", - [ - "arn:", - { - "Ref": "AWS::Partition" - }, - ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" - ] - ] + "Ref": "NotificationQueue36610CC1" } ] } }, - "SingletonLambda224e77f9a32e4b4dac32983477abba164533EA15": { - "Type": "AWS::Lambda::Function", + "NotificationQueueawscdksesreceiptTopicE9CA2388E8E96C33": { + "Type": "AWS::SNS::Subscription", "Properties": { - "Code": { - "S3Bucket": { - "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" - }, - "S3Key": "96d0b6be9a64ae309bf89a86f5515453f0fa1d07b4f6b37198051cc98e251f34.zip" - }, - "Handler": "index.handler", - "Role": { + "Endpoint": { "Fn::GetAtt": [ - "SingletonLambda224e77f9a32e4b4dac32983477abba16ServiceRole3037F5B4", + "NotificationQueue36610CC1", "Arn" ] }, - "Runtime": "nodejs18.x" + "Protocol": "sqs", + "TopicArn": { + "Ref": "TopicBFC7AF6E" + } }, "DependsOn": [ - "SingletonLambda224e77f9a32e4b4dac32983477abba16ServiceRole3037F5B4" + "NotificationQueuePolicyCC060EA6" ] - }, - "SingletonLambda224e77f9a32e4b4dac32983477abba16AllowSesB42DF904": { - "Type": "AWS::Lambda::Permission", - "Properties": { - "Action": "lambda:InvokeFunction", - "FunctionName": { - "Fn::GetAtt": [ - "SingletonLambda224e77f9a32e4b4dac32983477abba164533EA15", - "Arn" - ] - }, - "Principal": "ses.amazonaws.com", - "SourceAccount": { - "Ref": "AWS::AccountId" - } - } } }, "Parameters": { diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-ses-actions/test/integ.actions.js.snapshot/cdk.out b/packages/@aws-cdk-testing/framework-integ/test/aws-ses-actions/test/integ.actions.js.snapshot/cdk.out index 2313ab5436501..1f0068d32659a 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-ses-actions/test/integ.actions.js.snapshot/cdk.out +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-ses-actions/test/integ.actions.js.snapshot/cdk.out @@ -1 +1 @@ -{"version":"34.0.0"} \ No newline at end of file +{"version":"36.0.0"} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-ses-actions/test/integ.actions.js.snapshot/integ.json b/packages/@aws-cdk-testing/framework-integ/test/aws-ses-actions/test/integ.actions.js.snapshot/integ.json index 24e948080173a..5d7e10198b4ba 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-ses-actions/test/integ.actions.js.snapshot/integ.json +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-ses-actions/test/integ.actions.js.snapshot/integ.json @@ -1,5 +1,5 @@ { - "version": "34.0.0", + "version": "36.0.0", "testCases": { "integ.actions": { "stacks": [ diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-ses-actions/test/integ.actions.js.snapshot/manifest.json b/packages/@aws-cdk-testing/framework-integ/test/aws-ses-actions/test/integ.actions.js.snapshot/manifest.json index 8a12686ca5823..6f9536b92e4c3 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-ses-actions/test/integ.actions.js.snapshot/manifest.json +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-ses-actions/test/integ.actions.js.snapshot/manifest.json @@ -1,5 +1,5 @@ { - "version": "34.0.0", + "version": "36.0.0", "artifacts": { "aws-cdk-ses-receipt.assets": { "type": "cdk:asset-manifest", @@ -14,10 +14,11 @@ "environment": "aws://unknown-account/unknown-region", "properties": { "templateFile": "aws-cdk-ses-receipt.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}/ea7e677fcdb536a575f56f02244366de338d8c370dbf941f7020229798da8db7.json", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/558c1448bff6417f14bd77ab3ed5fda1c051566e3e95f20865a1b5d5ac53d66c.json", "requiresBootstrapStackVersion": 6, "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", "additionalDependencies": [ @@ -75,58 +76,97 @@ "data": "Key961B73FD" } ], - "/aws-cdk-ses-receipt/RuleSet/Resource": [ + "/aws-cdk-ses-receipt/RuleSet/FirstRule/Resource": [ { "type": "aws:cdk:logicalId", - "data": "RuleSetE30C6C48" + "data": "RuleSetFirstRule0A27C8CC", + "trace": [ + "!!DESTRUCTIVE_CHANGES: WILL_REPLACE" + ] } ], - "/aws-cdk-ses-receipt/RuleSet/DropSpam/Rule/Resource": [ + "/aws-cdk-ses-receipt/RuleSet/SecondRule/Resource": [ { "type": "aws:cdk:logicalId", - "data": "RuleSetDropSpamRule5809F51B" + "data": "RuleSetSecondRule03178AD4", + "trace": [ + "!!DESTRUCTIVE_CHANGES: WILL_REPLACE" + ] } ], - "/aws-cdk-ses-receipt/RuleSet/FirstRule/Resource": [ + "/aws-cdk-ses-receipt/NotificationQueue/Resource": [ { "type": "aws:cdk:logicalId", - "data": "RuleSetFirstRule0A27C8CC" + "data": "NotificationQueue36610CC1" } ], - "/aws-cdk-ses-receipt/RuleSet/SecondRule/Resource": [ + "/aws-cdk-ses-receipt/NotificationQueue/Policy/Resource": [ { "type": "aws:cdk:logicalId", - "data": "RuleSetSecondRule03178AD4" + "data": "NotificationQueuePolicyCC060EA6" } ], - "/aws-cdk-ses-receipt/SingletonLambda224e77f9a32e4b4dac32983477abba16/ServiceRole/Resource": [ + "/aws-cdk-ses-receipt/NotificationQueue/awscdksesreceiptTopicE9CA2388/Resource": [ { "type": "aws:cdk:logicalId", - "data": "SingletonLambda224e77f9a32e4b4dac32983477abba16ServiceRole3037F5B4" + "data": "NotificationQueueawscdksesreceiptTopicE9CA2388E8E96C33" } ], - "/aws-cdk-ses-receipt/SingletonLambda224e77f9a32e4b4dac32983477abba16/Resource": [ + "/aws-cdk-ses-receipt/BootstrapVersion": [ { "type": "aws:cdk:logicalId", - "data": "SingletonLambda224e77f9a32e4b4dac32983477abba164533EA15" + "data": "BootstrapVersion" } ], - "/aws-cdk-ses-receipt/SingletonLambda224e77f9a32e4b4dac32983477abba16/AllowSes": [ + "/aws-cdk-ses-receipt/CheckBootstrapVersion": [ { "type": "aws:cdk:logicalId", - "data": "SingletonLambda224e77f9a32e4b4dac32983477abba16AllowSesB42DF904" + "data": "CheckBootstrapVersion" } ], - "/aws-cdk-ses-receipt/BootstrapVersion": [ + "RuleSetE30C6C48": [ { "type": "aws:cdk:logicalId", - "data": "BootstrapVersion" + "data": "RuleSetE30C6C48", + "trace": [ + "!!DESTRUCTIVE_CHANGES: WILL_DESTROY" + ] } ], - "/aws-cdk-ses-receipt/CheckBootstrapVersion": [ + "RuleSetDropSpamRule5809F51B": [ { "type": "aws:cdk:logicalId", - "data": "CheckBootstrapVersion" + "data": "RuleSetDropSpamRule5809F51B", + "trace": [ + "!!DESTRUCTIVE_CHANGES: WILL_DESTROY" + ] + } + ], + "SingletonLambda224e77f9a32e4b4dac32983477abba16ServiceRole3037F5B4": [ + { + "type": "aws:cdk:logicalId", + "data": "SingletonLambda224e77f9a32e4b4dac32983477abba16ServiceRole3037F5B4", + "trace": [ + "!!DESTRUCTIVE_CHANGES: WILL_DESTROY" + ] + } + ], + "SingletonLambda224e77f9a32e4b4dac32983477abba164533EA15": [ + { + "type": "aws:cdk:logicalId", + "data": "SingletonLambda224e77f9a32e4b4dac32983477abba164533EA15", + "trace": [ + "!!DESTRUCTIVE_CHANGES: WILL_DESTROY" + ] + } + ], + "SingletonLambda224e77f9a32e4b4dac32983477abba16AllowSesB42DF904": [ + { + "type": "aws:cdk:logicalId", + "data": "SingletonLambda224e77f9a32e4b4dac32983477abba16AllowSesB42DF904", + "trace": [ + "!!DESTRUCTIVE_CHANGES: WILL_DESTROY" + ] } ] }, diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-ses-actions/test/integ.actions.js.snapshot/tree.json b/packages/@aws-cdk-testing/framework-integ/test/aws-ses-actions/test/integ.actions.js.snapshot/tree.json index c81f212eca6a6..7eadd11a1c964 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-ses-actions/test/integ.actions.js.snapshot/tree.json +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-ses-actions/test/integ.actions.js.snapshot/tree.json @@ -180,8 +180,31 @@ "Action": "s3:PutObject", "Condition": { "StringEquals": { - "aws:Referer": { + "aws:SourceAccount": { "Ref": "AWS::AccountId" + }, + "aws:SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ses:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":receipt-rule-set/INBOUND_MAIL:receipt-rule/", + { + "Ref": "RuleSetFirstRule0A27C8CC" + } + ] + ] } } }, @@ -303,79 +326,6 @@ "id": "RuleSet", "path": "aws-cdk-ses-receipt/RuleSet", "children": { - "Resource": { - "id": "Resource", - "path": "aws-cdk-ses-receipt/RuleSet/Resource", - "attributes": { - "aws:cdk:cloudformation:type": "AWS::SES::ReceiptRuleSet", - "aws:cdk:cloudformation:props": {} - }, - "constructInfo": { - "fqn": "aws-cdk-lib.aws_ses.CfnReceiptRuleSet", - "version": "0.0.0" - } - }, - "DropSpam": { - "id": "DropSpam", - "path": "aws-cdk-ses-receipt/RuleSet/DropSpam", - "children": { - "Function": { - "id": "Function", - "path": "aws-cdk-ses-receipt/RuleSet/DropSpam/Function", - "constructInfo": { - "fqn": "aws-cdk-lib.aws_lambda.SingletonFunction", - "version": "0.0.0" - } - }, - "Rule": { - "id": "Rule", - "path": "aws-cdk-ses-receipt/RuleSet/DropSpam/Rule", - "children": { - "Resource": { - "id": "Resource", - "path": "aws-cdk-ses-receipt/RuleSet/DropSpam/Rule/Resource", - "attributes": { - "aws:cdk:cloudformation:type": "AWS::SES::ReceiptRule", - "aws:cdk:cloudformation:props": { - "rule": { - "actions": [ - { - "lambdaAction": { - "functionArn": { - "Fn::GetAtt": [ - "SingletonLambda224e77f9a32e4b4dac32983477abba164533EA15", - "Arn" - ] - }, - "invocationType": "RequestResponse" - } - } - ], - "enabled": true, - "scanEnabled": true - }, - "ruleSetName": { - "Ref": "RuleSetE30C6C48" - } - } - }, - "constructInfo": { - "fqn": "aws-cdk-lib.aws_ses.CfnReceiptRule", - "version": "0.0.0" - } - } - }, - "constructInfo": { - "fqn": "aws-cdk-lib.aws_ses.ReceiptRule", - "version": "0.0.0" - } - } - }, - "constructInfo": { - "fqn": "aws-cdk-lib.aws_ses.DropSpamReceiptRule", - "version": "0.0.0" - } - }, "FirstRule": { "id": "FirstRule", "path": "aws-cdk-ses-receipt/RuleSet/FirstRule", @@ -386,9 +336,6 @@ "attributes": { "aws:cdk:cloudformation:type": "AWS::SES::ReceiptRule", "aws:cdk:cloudformation:props": { - "after": { - "Ref": "RuleSetDropSpamRule5809F51B" - }, "rule": { "actions": [ { @@ -438,7 +385,7 @@ }, { "bounceAction": { - "sender": "cdk-ses-receipt-test@yopmail.com", + "sender": "test@cdk-test-123.awsapps.com", "smtpReplyCode": "500", "message": "Message content rejected", "topicArn": { @@ -451,14 +398,12 @@ "enabled": true, "name": "FirstRule", "recipients": [ - "cdk-ses-receipt-test@yopmail.com" + "test@cdk-test-123.awsapps.com" ], "scanEnabled": true, "tlsPolicy": "Require" }, - "ruleSetName": { - "Ref": "RuleSetE30C6C48" - } + "ruleSetName": "INBOUND_MAIL" } }, "constructInfo": { @@ -498,9 +443,7 @@ ], "enabled": true }, - "ruleSetName": { - "Ref": "RuleSetE30C6C48" - } + "ruleSetName": "INBOUND_MAIL" } }, "constructInfo": { @@ -516,151 +459,115 @@ } }, "constructInfo": { - "fqn": "aws-cdk-lib.aws_ses.ReceiptRuleSet", + "fqn": "aws-cdk-lib.Resource", "version": "0.0.0" } }, - "SingletonLambda224e77f9a32e4b4dac32983477abba16": { - "id": "SingletonLambda224e77f9a32e4b4dac32983477abba16", - "path": "aws-cdk-ses-receipt/SingletonLambda224e77f9a32e4b4dac32983477abba16", + "NotificationQueue": { + "id": "NotificationQueue", + "path": "aws-cdk-ses-receipt/NotificationQueue", "children": { - "ServiceRole": { - "id": "ServiceRole", - "path": "aws-cdk-ses-receipt/SingletonLambda224e77f9a32e4b4dac32983477abba16/ServiceRole", + "Resource": { + "id": "Resource", + "path": "aws-cdk-ses-receipt/NotificationQueue/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::SQS::Queue", + "aws:cdk:cloudformation:props": {} + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_sqs.CfnQueue", + "version": "0.0.0" + } + }, + "Policy": { + "id": "Policy", + "path": "aws-cdk-ses-receipt/NotificationQueue/Policy", "children": { - "ImportServiceRole": { - "id": "ImportServiceRole", - "path": "aws-cdk-ses-receipt/SingletonLambda224e77f9a32e4b4dac32983477abba16/ServiceRole/ImportServiceRole", - "constructInfo": { - "fqn": "aws-cdk-lib.Resource", - "version": "0.0.0" - } - }, "Resource": { "id": "Resource", - "path": "aws-cdk-ses-receipt/SingletonLambda224e77f9a32e4b4dac32983477abba16/ServiceRole/Resource", + "path": "aws-cdk-ses-receipt/NotificationQueue/Policy/Resource", "attributes": { - "aws:cdk:cloudformation:type": "AWS::IAM::Role", + "aws:cdk:cloudformation:type": "AWS::SQS::QueuePolicy", "aws:cdk:cloudformation:props": { - "assumeRolePolicyDocument": { + "policyDocument": { "Statement": [ { - "Action": "sts:AssumeRole", + "Action": "sqs:SendMessage", + "Condition": { + "ArnEquals": { + "aws:SourceArn": { + "Ref": "TopicBFC7AF6E" + } + } + }, "Effect": "Allow", "Principal": { - "Service": "lambda.amazonaws.com" + "Service": "sns.amazonaws.com" + }, + "Resource": { + "Fn::GetAtt": [ + "NotificationQueue36610CC1", + "Arn" + ] } } ], "Version": "2012-10-17" }, - "managedPolicyArns": [ + "queues": [ { - "Fn::Join": [ - "", - [ - "arn:", - { - "Ref": "AWS::Partition" - }, - ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" - ] - ] + "Ref": "NotificationQueue36610CC1" } ] } }, "constructInfo": { - "fqn": "aws-cdk-lib.aws_iam.CfnRole", + "fqn": "aws-cdk-lib.aws_sqs.CfnQueuePolicy", "version": "0.0.0" } } }, "constructInfo": { - "fqn": "aws-cdk-lib.aws_iam.Role", + "fqn": "aws-cdk-lib.aws_sqs.QueuePolicy", "version": "0.0.0" } }, - "Code": { - "id": "Code", - "path": "aws-cdk-ses-receipt/SingletonLambda224e77f9a32e4b4dac32983477abba16/Code", + "awscdksesreceiptTopicE9CA2388": { + "id": "awscdksesreceiptTopicE9CA2388", + "path": "aws-cdk-ses-receipt/NotificationQueue/awscdksesreceiptTopicE9CA2388", "children": { - "Stage": { - "id": "Stage", - "path": "aws-cdk-ses-receipt/SingletonLambda224e77f9a32e4b4dac32983477abba16/Code/Stage", - "constructInfo": { - "fqn": "aws-cdk-lib.AssetStaging", - "version": "0.0.0" - } - }, - "AssetBucket": { - "id": "AssetBucket", - "path": "aws-cdk-ses-receipt/SingletonLambda224e77f9a32e4b4dac32983477abba16/Code/AssetBucket", + "Resource": { + "id": "Resource", + "path": "aws-cdk-ses-receipt/NotificationQueue/awscdksesreceiptTopicE9CA2388/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::SNS::Subscription", + "aws:cdk:cloudformation:props": { + "endpoint": { + "Fn::GetAtt": [ + "NotificationQueue36610CC1", + "Arn" + ] + }, + "protocol": "sqs", + "topicArn": { + "Ref": "TopicBFC7AF6E" + } + } + }, "constructInfo": { - "fqn": "aws-cdk-lib.aws_s3.BucketBase", + "fqn": "aws-cdk-lib.aws_sns.CfnSubscription", "version": "0.0.0" } } }, "constructInfo": { - "fqn": "aws-cdk-lib.aws_s3_assets.Asset", - "version": "0.0.0" - } - }, - "Resource": { - "id": "Resource", - "path": "aws-cdk-ses-receipt/SingletonLambda224e77f9a32e4b4dac32983477abba16/Resource", - "attributes": { - "aws:cdk:cloudformation:type": "AWS::Lambda::Function", - "aws:cdk:cloudformation:props": { - "code": { - "s3Bucket": { - "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" - }, - "s3Key": "96d0b6be9a64ae309bf89a86f5515453f0fa1d07b4f6b37198051cc98e251f34.zip" - }, - "handler": "index.handler", - "role": { - "Fn::GetAtt": [ - "SingletonLambda224e77f9a32e4b4dac32983477abba16ServiceRole3037F5B4", - "Arn" - ] - }, - "runtime": "nodejs18.x" - } - }, - "constructInfo": { - "fqn": "aws-cdk-lib.aws_lambda.CfnFunction", - "version": "0.0.0" - } - }, - "AllowSes": { - "id": "AllowSes", - "path": "aws-cdk-ses-receipt/SingletonLambda224e77f9a32e4b4dac32983477abba16/AllowSes", - "attributes": { - "aws:cdk:cloudformation:type": "AWS::Lambda::Permission", - "aws:cdk:cloudformation:props": { - "action": "lambda:InvokeFunction", - "functionName": { - "Fn::GetAtt": [ - "SingletonLambda224e77f9a32e4b4dac32983477abba164533EA15", - "Arn" - ] - }, - "principal": "ses.amazonaws.com", - "sourceAccount": { - "Ref": "AWS::AccountId" - } - } - }, - "constructInfo": { - "fqn": "aws-cdk-lib.aws_lambda.CfnPermission", + "fqn": "aws-cdk-lib.aws_sns.Subscription", "version": "0.0.0" } } }, "constructInfo": { - "fqn": "aws-cdk-lib.aws_lambda.Function", + "fqn": "aws-cdk-lib.aws_sqs.Queue", "version": "0.0.0" } }, @@ -691,7 +598,7 @@ "path": "Tree", "constructInfo": { "fqn": "constructs.Construct", - "version": "10.2.70" + "version": "10.3.0" } } }, diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-ses-actions/test/integ.actions.ts b/packages/@aws-cdk-testing/framework-integ/test/aws-ses-actions/test/integ.actions.ts index 00d5ce36cbef5..93f0f4487b65f 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-ses-actions/test/integ.actions.ts +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-ses-actions/test/integ.actions.ts @@ -4,18 +4,23 @@ import * as s3 from 'aws-cdk-lib/aws-s3'; import * as ses from 'aws-cdk-lib/aws-ses'; import * as sns from 'aws-cdk-lib/aws-sns'; import * as cdk from 'aws-cdk-lib'; +import * as subscriptions from 'aws-cdk-lib/aws-sns-subscriptions'; +import * as sqs from 'aws-cdk-lib/aws-sqs'; import * as actions from 'aws-cdk-lib/aws-ses-actions'; import { STANDARD_NODEJS_RUNTIME } from '../../config'; -/********************************************************************************************************************** - * - * Warning! This test case can not be deployed! - * - * Save yourself some time and move on. - * The latest given reason is: - * - 2023-08-30: Uses a hardcoded email address that is not verified, @mrgrain - * - *********************************************************************************************************************/ +/** + * 1. Create a free Workmail test domain (https://us-east-1.console.aws.amazon.com/workmail/v2/home?region=us-east-1#/organizations/create) + * - It should automatically be added to your list of verified SES domains, no need to exit the SES sandbox + * 2. Add a new user email address in the Workmail console + * 3. Update the TEST_EMAIL constant with the email address of the user you created + * 4. Deploy the stack with --no-clean, and send an email to the email address you created + * 5. Check the following: + * - The email should be saved to the S3 bucket + * - The SQS queue should receive receipt notifications + */ + +const TEST_EMAIL = 'test@cdk-test-123.awsapps.com'; const app = new cdk.App(); @@ -33,9 +38,12 @@ const bucket = new s3.Bucket(stack, 'Bucket'); const kmsKey = new kms.Key(stack, 'Key'); -const ruleSet = new ses.ReceiptRuleSet(stack, 'RuleSet', { - dropSpam: true, -}); +const ruleSet = ses.ReceiptRuleSet.fromReceiptRuleSetName( + stack, + 'RuleSet', + // Default WorkMail rule set + 'INBOUND_MAIL', +); const firstRule = ruleSet.addRule('FirstRule', { actions: [ @@ -60,13 +68,13 @@ const firstRule = ruleSet.addRule('FirstRule', { }), ], receiptRuleName: 'FirstRule', - recipients: ['cdk-ses-receipt-test@yopmail.com'], + recipients: [TEST_EMAIL], scanEnabled: true, tlsPolicy: ses.TlsPolicy.REQUIRE, }); firstRule.addAction(new actions.Bounce({ - sender: 'cdk-ses-receipt-test@yopmail.com', + sender: TEST_EMAIL, template: actions.BounceTemplate.MESSAGE_CONTENT_REJECTED, topic, })); @@ -77,4 +85,7 @@ secondRule.addAction(new actions.Stop({ topic, })); +const queue = new sqs.Queue(stack, 'NotificationQueue'); +topic.addSubscription(new subscriptions.SqsSubscription(queue)); + app.synth(); diff --git a/packages/aws-cdk-lib/aws-ses-actions/lib/s3.ts b/packages/aws-cdk-lib/aws-ses-actions/lib/s3.ts index 5fa01ce1c91be..156a50813f298 100644 --- a/packages/aws-cdk-lib/aws-ses-actions/lib/s3.ts +++ b/packages/aws-cdk-lib/aws-ses-actions/lib/s3.ts @@ -42,31 +42,12 @@ export interface S3Props { * a notification to Amazon SNS. */ export class S3 implements ses.IReceiptRuleAction { + private rule?: ses.IReceiptRule; constructor(private readonly props: S3Props) { } public bind(rule: ses.IReceiptRule): ses.ReceiptRuleActionConfig { - // Allow SES to write to S3 bucket - // See https://docs.aws.amazon.com/ses/latest/DeveloperGuide/receiving-email-permissions.html#receiving-email-permissions-s3 - const keyPattern = this.props.objectKeyPrefix || ''; - const s3Statement = new iam.PolicyStatement({ - actions: ['s3:PutObject'], - principals: [new iam.ServicePrincipal('ses.amazonaws.com')], - resources: [this.props.bucket.arnForObjects(`${keyPattern}*`)], - conditions: { - StringEquals: { - 'aws:Referer': cdk.Aws.ACCOUNT_ID, - }, - }, - }); - this.props.bucket.addToResourcePolicy(s3Statement); - - const policy = this.props.bucket.node.tryFindChild('Policy') as s3.BucketPolicy; - if (policy) { // The bucket could be imported - rule.node.addDependency(policy); - } else { - cdk.Annotations.of(rule).addWarningV2('@aws-cdk/s3:AddBucketPermissions', 'This rule is using a S3 action with an imported bucket. Ensure permission is given to SES to write to that bucket.'); - } + this.rule = rule; // Allow SES to use KMS master key // See https://docs.aws.amazon.com/ses/latest/DeveloperGuide/receiving-email-permissions.html#receiving-email-permissions-kms @@ -98,4 +79,41 @@ export class S3 implements ses.IReceiptRuleAction { }, }; } + + /** + * Generate and apply the receipt rule action statement + * + * @param ruleSet The rule set the rule is being added to + * @internal + */ + public _applyPolicyStatement(receiptRuleSet: ses.IReceiptRuleSet): void { + if (!this.rule) { + throw new Error('Cannot apply policy statement before binding the action to a receipt rule'); + } + + // Allow SES to write to S3 bucket + // See https://docs.aws.amazon.com/ses/latest/DeveloperGuide/receiving-email-permissions.html#receiving-email-permissions-s3 + const keyPattern = this.props.objectKeyPrefix || ''; + const s3Statement = new iam.PolicyStatement({ + actions: ['s3:PutObject'], + principals: [new iam.ServicePrincipal('ses.amazonaws.com')], + resources: [this.props.bucket.arnForObjects(`${keyPattern}*`)], + conditions: { + StringEquals: { + 'aws:SourceAccount': cdk.Aws.ACCOUNT_ID, + 'aws:SourceArn': cdk.Arn.format({ + partition: cdk.Aws.PARTITION, + service: 'ses', + region: cdk.Aws.REGION, + account: cdk.Aws.ACCOUNT_ID, + resource: [ + `receipt-rule-set/${receiptRuleSet.receiptRuleSetName}`, + `receipt-rule/${this.rule.receiptRuleName}`, + ].join(':'), + }), + }, + }, + }); + this.props.bucket.addToResourcePolicy(s3Statement); + } } diff --git a/packages/aws-cdk-lib/aws-ses-actions/test/actions.test.ts b/packages/aws-cdk-lib/aws-ses-actions/test/actions.test.ts index f53bf29a2306b..d9b7c143f70ac 100644 --- a/packages/aws-cdk-lib/aws-ses-actions/test/actions.test.ts +++ b/packages/aws-cdk-lib/aws-ses-actions/test/actions.test.ts @@ -178,9 +178,6 @@ test('add s3 action', () => { Ref: 'RuleSetE30C6C48', }, }, - DependsOn: [ - 'BucketPolicyE9A3008A', - ], }); Template.fromStack(stack).hasResourceProperties('AWS::S3::BucketPolicy', { @@ -193,9 +190,26 @@ test('add s3 action', () => { Action: 's3:PutObject', Condition: { StringEquals: { - 'aws:Referer': { + 'aws:SourceAccount': { Ref: 'AWS::AccountId', }, + 'aws:SourceArn': { + 'Fn::Join': [ + '', + [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':ses:', + { Ref: 'AWS::Region' }, + ':', + { Ref: 'AWS::AccountId' }, + ':receipt-rule-set/', + { Ref: 'RuleSetE30C6C48' }, + ':receipt-rule/', + { Ref: 'RuleSetRule0B1D6BCA' }, + ], + ], + }, }, }, Effect: 'Allow', diff --git a/packages/aws-cdk-lib/aws-ses/lib/receipt-rule-action.ts b/packages/aws-cdk-lib/aws-ses/lib/receipt-rule-action.ts index ab7dd354df171..8e95fb714c19c 100644 --- a/packages/aws-cdk-lib/aws-ses/lib/receipt-rule-action.ts +++ b/packages/aws-cdk-lib/aws-ses/lib/receipt-rule-action.ts @@ -1,4 +1,5 @@ import { IReceiptRule } from './receipt-rule'; +import { IReceiptRuleSet } from './receipt-rule-set'; /** * An abstract action for a receipt rule. @@ -8,6 +9,14 @@ export interface IReceiptRuleAction { * Returns the receipt rule action specification */ bind(receiptRule: IReceiptRule): ReceiptRuleActionConfig; + + /** + * Generate and apply the receipt rule action statement + * + * @param ruleSet The rule set the rule is being added to + * @internal + */ + _applyPolicyStatement?(ruleSet: IReceiptRuleSet): void; } /** diff --git a/packages/aws-cdk-lib/aws-ses/lib/receipt-rule.ts b/packages/aws-cdk-lib/aws-ses/lib/receipt-rule.ts index ae662eb3f18d4..5b9dbe89f8c4a 100644 --- a/packages/aws-cdk-lib/aws-ses/lib/receipt-rule.ts +++ b/packages/aws-cdk-lib/aws-ses/lib/receipt-rule.ts @@ -112,7 +112,10 @@ export class ReceiptRule extends Resource implements IReceiptRule { } public readonly receiptRuleName: string; - private readonly actions = new Array(); + + private readonly ruleSet: IReceiptRuleSet; + private readonly actions: IReceiptRuleAction[] = []; + private readonly actionProperties: CfnReceiptRule.ActionProperty[] = []; constructor(scope: Construct, id: string, props: ReceiptRuleProps) { super(scope, id, { @@ -133,6 +136,7 @@ export class ReceiptRule extends Resource implements IReceiptRule { }); this.receiptRuleName = resource.ref; + this.ruleSet = props.ruleSet; for (const action of props.actions || []) { this.addAction(action); @@ -143,15 +147,20 @@ export class ReceiptRule extends Resource implements IReceiptRule { * Adds an action to this receipt rule. */ public addAction(action: IReceiptRuleAction) { - this.actions.push(action.bind(this)); + this.actions.push(action); + this.actionProperties.push(action.bind(this)); } private renderActions() { - if (this.actions.length === 0) { + if (this.actionProperties.length === 0) { return undefined; } - return this.actions; + for (const action of this.actions) { + action._applyPolicyStatement?.(this.ruleSet); + } + + return this.actionProperties; } }