From 9eb21bd79ca69a1f3f0cabf7d31810941f17da8b Mon Sep 17 00:00:00 2001 From: AWS CDK Automation <43080478+aws-cdk-automation@users.noreply.github.com> Date: Fri, 19 Apr 2024 15:53:54 -0700 Subject: [PATCH 1/2] chore(cli-testing): add a retry for test (#29908) One of our tests can remove customPermissionsBoundary creates a policy using createPolicy. Change to IAM policies/roles use eventual consistency. So, while the changes will show up right away if we were to call an API to describe that policy/role, the updates may not have actually propagated to all regions yet. This is likely the cause of the intermittent test failures for this test. This change adds the eventually block and uses it to retry initial creation of this stack in the case that the policy changes have not made it to the relevant region just yet. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../cli-integ/lib/eventually.ts | 42 +++++++++++++++++++ .../bootstrapping.integtest.ts | 29 ++++++++----- 2 files changed, 61 insertions(+), 10 deletions(-) create mode 100644 packages/@aws-cdk-testing/cli-integ/lib/eventually.ts 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 From 2da544feeeda68a379f0f79f18e138b9640c1691 Mon Sep 17 00:00:00 2001 From: Jimmy Gaussen Date: Sat, 20 Apr 2024 01:23:45 +0200 Subject: [PATCH 2/2] fix(ses-actions): permissions too wide for S3 action (#29833) ### Issue # (if applicable) Closes #29811, continuation of @msambol 's #29823 ### Reason for this change Reduce overly broad permissions allocated to SES for the S3 receipt rule action ### Description of changes * Restrain by both rule set and rule name, as recommended in the [docs](https://docs.aws.amazon.com/ses/latest/dg/receiving-email-permissions.html#receiving-email-permissions-s3) * Accomplished by generating the permission lazily, when the rule is rendering the actions for CloudFormation ### Description of how you validated changes Updated the unit and integration tests. The integration now uses a free test WorkMail domain. It's a bit of manual setup upfront, but doesn't require the contributor to use one of their own domains ### Checklist - [x] My code adheres to the [CONTRIBUTING GUIDE](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md) and [DESIGN GUIDELINES](https://github.com/aws/aws-cdk/blob/main/docs/DESIGN_GUIDELINES.md) ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../index.d.ts | 3 - .../index.js | 22 -- .../index.ts | 22 -- .../aws-cdk-ses-receipt.assets.json | 19 +- .../aws-cdk-ses-receipt.template.json | 148 ++++----- .../test/integ.actions.js.snapshot/cdk.out | 2 +- .../test/integ.actions.js.snapshot/integ.json | 2 +- .../integ.actions.js.snapshot/manifest.json | 80 +++-- .../test/integ.actions.js.snapshot/tree.json | 281 ++++++------------ .../aws-ses-actions/test/integ.actions.ts | 39 ++- .../aws-cdk-lib/aws-ses-actions/lib/s3.ts | 60 ++-- .../aws-ses-actions/test/actions.test.ts | 22 +- .../aws-ses/lib/receipt-rule-action.ts | 9 + .../aws-cdk-lib/aws-ses/lib/receipt-rule.ts | 17 +- 14 files changed, 325 insertions(+), 401 deletions(-) delete mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-ses-actions/test/integ.actions.js.snapshot/asset.96d0b6be9a64ae309bf89a86f5515453f0fa1d07b4f6b37198051cc98e251f34/index.d.ts delete mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-ses-actions/test/integ.actions.js.snapshot/asset.96d0b6be9a64ae309bf89a86f5515453f0fa1d07b4f6b37198051cc98e251f34/index.js delete mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-ses-actions/test/integ.actions.js.snapshot/asset.96d0b6be9a64ae309bf89a86f5515453f0fa1d07b4f6b37198051cc98e251f34/index.ts 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; } }