From c3c771c6f6f6790f2298a85a549bded640d2e35b Mon Sep 17 00:00:00 2001 From: Kara Potts Date: Tue, 13 Feb 2024 16:22:53 +0000 Subject: [PATCH] feat(ses): `grant` methods to `IEmailIdentity` (#29084) ### Issue Closes #29083 ### Reason for this change When granting send email access to a lambda the grant needs to be constructed manually, including constructing the ARN for the identity. e.g. ``` Grant.addToPrincipal({ grantee, actions: ["ses:SendEmail"], resourceArns: [ this.stack.formatArn({ service: 'ses', resource: 'identity', resourceName: 'test@example.com', }), ], scope: this }) ``` This is dissimilar to other constructs, which generally expose a grant method and one or more convenience methods for particularly relevant groups of actions. ### Description of changes Added `grant` and `grantSendEmail` to `IEmailIdentity`, and added a common abstract class, `BaseEmailIdentity` with the relevant grant code. This is to avoid code duplication between the full `EmailIdentity` and the `Import` class. ### Description of how you validated changes Tests added for grants on both new and imported email identities, and a test to validate the `grantSendEmail` method. ### 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* --- .../aws-ses/test/fixtures/send-email/index.py | 31 ++ ...efaultTestDeployAssert3F909307.assets.json | 2 +- .../index.py | 31 ++ .../cdk-ses-email-identity-integ.assets.json | 19 +- ...cdk-ses-email-identity-integ.template.json | 143 ++++++-- .../integ.email-identity.js.snapshot/cdk.out | 2 +- .../integ.json | 5 +- .../manifest.json | 36 +- .../tree.json | 317 +++++++++++++++--- .../test/aws-ses/test/integ.email-identity.ts | 13 +- packages/aws-cdk-lib/aws-ses/README.md | 16 + .../aws-cdk-lib/aws-ses/lib/email-identity.ts | 86 ++++- .../aws-ses/test/email-identity.test.ts | 136 ++++++++ 13 files changed, 749 insertions(+), 88 deletions(-) create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-ses/test/fixtures/send-email/index.py create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-ses/test/integ.email-identity.js.snapshot/asset.ab3156800c2f322f16da8bff913172139189a387dd64a4e622f82a790561fd4d/index.py diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-ses/test/fixtures/send-email/index.py b/packages/@aws-cdk-testing/framework-integ/test/aws-ses/test/fixtures/send-email/index.py new file mode 100644 index 0000000000000..d6e4b468282eb --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-ses/test/fixtures/send-email/index.py @@ -0,0 +1,31 @@ +import json +import boto3 + +client = boto3.client('ses', region_name='us-west-2') + +def lambda_handler(event, context): + response = client.send_email( + Destination={ + 'ToAddresses': ['test@example.com'] + }, + Message={ + 'Body': { + 'Text': { + 'Charset': 'UTF-8', + 'Data': 'This is the message body in text format.', + } + }, + 'Subject': { + 'Charset': 'UTF-8', + 'Data': 'Test email', + }, + }, + Source='sender@cdk.dev' + ) + + print(response) + + return { + 'statusCode': 200, + 'body': json.dumps("Email Sent Successfully. MessageId is: " + response['MessageId']) + } \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-ses/test/integ.email-identity.js.snapshot/EmailIdentityIntegDefaultTestDeployAssert3F909307.assets.json b/packages/@aws-cdk-testing/framework-integ/test/aws-ses/test/integ.email-identity.js.snapshot/EmailIdentityIntegDefaultTestDeployAssert3F909307.assets.json index 7044ba1f72377..16de6267b3547 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-ses/test/integ.email-identity.js.snapshot/EmailIdentityIntegDefaultTestDeployAssert3F909307.assets.json +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-ses/test/integ.email-identity.js.snapshot/EmailIdentityIntegDefaultTestDeployAssert3F909307.assets.json @@ -1,5 +1,5 @@ { - "version": "20.0.0", + "version": "36.0.0", "files": { "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22": { "source": { diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-ses/test/integ.email-identity.js.snapshot/asset.ab3156800c2f322f16da8bff913172139189a387dd64a4e622f82a790561fd4d/index.py b/packages/@aws-cdk-testing/framework-integ/test/aws-ses/test/integ.email-identity.js.snapshot/asset.ab3156800c2f322f16da8bff913172139189a387dd64a4e622f82a790561fd4d/index.py new file mode 100644 index 0000000000000..d6e4b468282eb --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-ses/test/integ.email-identity.js.snapshot/asset.ab3156800c2f322f16da8bff913172139189a387dd64a4e622f82a790561fd4d/index.py @@ -0,0 +1,31 @@ +import json +import boto3 + +client = boto3.client('ses', region_name='us-west-2') + +def lambda_handler(event, context): + response = client.send_email( + Destination={ + 'ToAddresses': ['test@example.com'] + }, + Message={ + 'Body': { + 'Text': { + 'Charset': 'UTF-8', + 'Data': 'This is the message body in text format.', + } + }, + 'Subject': { + 'Charset': 'UTF-8', + 'Data': 'Test email', + }, + }, + Source='sender@cdk.dev' + ) + + print(response) + + return { + 'statusCode': 200, + 'body': json.dumps("Email Sent Successfully. MessageId is: " + response['MessageId']) + } \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-ses/test/integ.email-identity.js.snapshot/cdk-ses-email-identity-integ.assets.json b/packages/@aws-cdk-testing/framework-integ/test/aws-ses/test/integ.email-identity.js.snapshot/cdk-ses-email-identity-integ.assets.json index 50a1078fb9f8e..97e015957f009 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-ses/test/integ.email-identity.js.snapshot/cdk-ses-email-identity-integ.assets.json +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-ses/test/integ.email-identity.js.snapshot/cdk-ses-email-identity-integ.assets.json @@ -1,7 +1,20 @@ { - "version": "20.0.0", + "version": "36.0.0", "files": { - "3461fb122e984a82b0e44e4b1b516ea2f581c705cce4e8f99d2001a9407cd2fa": { + "ab3156800c2f322f16da8bff913172139189a387dd64a4e622f82a790561fd4d": { + "source": { + "path": "asset.ab3156800c2f322f16da8bff913172139189a387dd64a4e622f82a790561fd4d", + "packaging": "zip" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "ab3156800c2f322f16da8bff913172139189a387dd64a4e622f82a790561fd4d.zip", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + }, + "8c1c73fb1dcf73cfd07d1697a478ad6e1fbe3673f2d10d7c35dbcaedc31ea460": { "source": { "path": "cdk-ses-email-identity-integ.template.json", "packaging": "file" @@ -9,7 +22,7 @@ "destinations": { "current_account-current_region": { "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", - "objectKey": "3461fb122e984a82b0e44e4b1b516ea2f581c705cce4e8f99d2001a9407cd2fa.json", + "objectKey": "8c1c73fb1dcf73cfd07d1697a478ad6e1fbe3673f2d10d7c35dbcaedc31ea460.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/test/integ.email-identity.js.snapshot/cdk-ses-email-identity-integ.template.json b/packages/@aws-cdk-testing/framework-integ/test/aws-ses/test/integ.email-identity.js.snapshot/cdk-ses-email-identity-integ.template.json index 176c7cdb798e2..97aef55c55df7 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-ses/test/integ.email-identity.js.snapshot/cdk-ses-email-identity-integ.template.json +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-ses/test/integ.email-identity.js.snapshot/cdk-ses-email-identity-integ.template.json @@ -6,19 +6,119 @@ "Name": "cdk.dev." } }, + "FunctionServiceRole675BB04A": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + } + }, + "FunctionServiceRoleDefaultPolicy2F49994A": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "ses:SendEmail", + "ses:SendRawEmail" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ses:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":identity/", + { + "Ref": "EmailIdentity7187767D" + } + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "FunctionServiceRoleDefaultPolicy2F49994A", + "Roles": [ + { + "Ref": "FunctionServiceRole675BB04A" + } + ] + } + }, + "Function76856677": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" + }, + "S3Key": "ab3156800c2f322f16da8bff913172139189a387dd64a4e622f82a790561fd4d.zip" + }, + "FunctionName": "email-sending-lambda", + "Handler": "index.lambda_handler", + "Role": { + "Fn::GetAtt": [ + "FunctionServiceRole675BB04A", + "Arn" + ] + }, + "Runtime": "python3.11" + }, + "DependsOn": [ + "FunctionServiceRoleDefaultPolicy2F49994A", + "FunctionServiceRole675BB04A" + ] + }, "EmailIdentityDkimDnsToken1BA32ACB3": { "Type": "AWS::Route53::RecordSet", "Properties": { + "HostedZoneId": { + "Ref": "HostedZoneDB99F866" + }, "Name": { "Fn::GetAtt": [ "EmailIdentity7187767D", "DkimDNSTokenName1" ] }, - "Type": "CNAME", - "HostedZoneId": { - "Ref": "HostedZoneDB99F866" - }, "ResourceRecords": [ { "Fn::GetAtt": [ @@ -27,22 +127,22 @@ ] } ], - "TTL": "1800" + "TTL": "1800", + "Type": "CNAME" } }, "EmailIdentityDkimDnsToken2BBEBB8EC": { "Type": "AWS::Route53::RecordSet", "Properties": { + "HostedZoneId": { + "Ref": "HostedZoneDB99F866" + }, "Name": { "Fn::GetAtt": [ "EmailIdentity7187767D", "DkimDNSTokenName2" ] }, - "Type": "CNAME", - "HostedZoneId": { - "Ref": "HostedZoneDB99F866" - }, "ResourceRecords": [ { "Fn::GetAtt": [ @@ -51,22 +151,22 @@ ] } ], - "TTL": "1800" + "TTL": "1800", + "Type": "CNAME" } }, "EmailIdentityDkimDnsToken3BB5E8A49": { "Type": "AWS::Route53::RecordSet", "Properties": { + "HostedZoneId": { + "Ref": "HostedZoneDB99F866" + }, "Name": { "Fn::GetAtt": [ "EmailIdentity7187767D", "DkimDNSTokenName3" ] }, - "Type": "CNAME", - "HostedZoneId": { - "Ref": "HostedZoneDB99F866" - }, "ResourceRecords": [ { "Fn::GetAtt": [ @@ -75,7 +175,8 @@ ] } ], - "TTL": "1800" + "TTL": "1800", + "Type": "CNAME" } }, "EmailIdentity7187767D": { @@ -90,11 +191,10 @@ "EmailIdentityMailFromMxRecordCEAAECD0": { "Type": "AWS::Route53::RecordSet", "Properties": { - "Name": "mail.cdk.dev.", - "Type": "MX", "HostedZoneId": { "Ref": "HostedZoneDB99F866" }, + "Name": "mail.cdk.dev.", "ResourceRecords": [ { "Fn::Join": [ @@ -109,21 +209,22 @@ ] } ], - "TTL": "1800" + "TTL": "1800", + "Type": "MX" } }, "EmailIdentityMailFromTxtRecordE6B5E5D0": { "Type": "AWS::Route53::RecordSet", "Properties": { - "Name": "mail.cdk.dev.", - "Type": "TXT", "HostedZoneId": { "Ref": "HostedZoneDB99F866" }, + "Name": "mail.cdk.dev.", "ResourceRecords": [ "\"v=spf1 include:amazonses.com ~all\"" ], - "TTL": "1800" + "TTL": "1800", + "Type": "TXT" } } }, diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-ses/test/integ.email-identity.js.snapshot/cdk.out b/packages/@aws-cdk-testing/framework-integ/test/aws-ses/test/integ.email-identity.js.snapshot/cdk.out index 588d7b269d34f..1f0068d32659a 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-ses/test/integ.email-identity.js.snapshot/cdk.out +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-ses/test/integ.email-identity.js.snapshot/cdk.out @@ -1 +1 @@ -{"version":"20.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/test/integ.email-identity.js.snapshot/integ.json b/packages/@aws-cdk-testing/framework-integ/test/aws-ses/test/integ.email-identity.js.snapshot/integ.json index 9e7358acc260d..fd772ae904c6e 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-ses/test/integ.email-identity.js.snapshot/integ.json +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-ses/test/integ.email-identity.js.snapshot/integ.json @@ -1,11 +1,12 @@ { - "version": "20.0.0", + "version": "36.0.0", "testCases": { "EmailIdentityInteg/DefaultTest": { "stacks": [ "cdk-ses-email-identity-integ" ], - "assertionStack": "EmailIdentityInteg/DefaultTest/DeployAssert" + "assertionStack": "EmailIdentityInteg/DefaultTest/DeployAssert", + "assertionStackName": "EmailIdentityIntegDefaultTestDeployAssert3F909307" } } } \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-ses/test/integ.email-identity.js.snapshot/manifest.json b/packages/@aws-cdk-testing/framework-integ/test/aws-ses/test/integ.email-identity.js.snapshot/manifest.json index a4474de28ad39..6362d8703aa00 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-ses/test/integ.email-identity.js.snapshot/manifest.json +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-ses/test/integ.email-identity.js.snapshot/manifest.json @@ -1,12 +1,6 @@ { - "version": "20.0.0", + "version": "36.0.0", "artifacts": { - "Tree": { - "type": "cdk:tree", - "properties": { - "file": "tree.json" - } - }, "cdk-ses-email-identity-integ.assets": { "type": "cdk:asset-manifest", "properties": { @@ -20,10 +14,11 @@ "environment": "aws://unknown-account/unknown-region", "properties": { "templateFile": "cdk-ses-email-identity-integ.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}/3461fb122e984a82b0e44e4b1b516ea2f581c705cce4e8f99d2001a9407cd2fa.json", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/8c1c73fb1dcf73cfd07d1697a478ad6e1fbe3673f2d10d7c35dbcaedc31ea460.json", "requiresBootstrapStackVersion": 6, "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", "additionalDependencies": [ @@ -45,6 +40,24 @@ "data": "HostedZoneDB99F866" } ], + "/cdk-ses-email-identity-integ/Function/ServiceRole/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "FunctionServiceRole675BB04A" + } + ], + "/cdk-ses-email-identity-integ/Function/ServiceRole/DefaultPolicy/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "FunctionServiceRoleDefaultPolicy2F49994A" + } + ], + "/cdk-ses-email-identity-integ/Function/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "Function76856677" + } + ], "/cdk-ses-email-identity-integ/EmailIdentity/DkimDnsToken1": [ { "type": "aws:cdk:logicalId", @@ -109,6 +122,7 @@ "environment": "aws://unknown-account/unknown-region", "properties": { "templateFile": "EmailIdentityIntegDefaultTestDeployAssert3F909307.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}", @@ -142,6 +156,12 @@ ] }, "displayName": "EmailIdentityInteg/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-ses/test/integ.email-identity.js.snapshot/tree.json b/packages/@aws-cdk-testing/framework-integ/test/aws-ses/test/integ.email-identity.js.snapshot/tree.json index e6753ebfb565e..cfa1021df9c5d 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-ses/test/integ.email-identity.js.snapshot/tree.json +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-ses/test/integ.email-identity.js.snapshot/tree.json @@ -4,14 +4,6 @@ "id": "App", "path": "", "children": { - "Tree": { - "id": "Tree", - "path": "Tree", - "constructInfo": { - "fqn": "constructs.Construct", - "version": "10.1.85" - } - }, "cdk-ses-email-identity-integ": { "id": "cdk-ses-email-identity-integ", "path": "cdk-ses-email-identity-integ", @@ -30,13 +22,198 @@ } }, "constructInfo": { - "fqn": "@aws-cdk/aws-route53.CfnHostedZone", + "fqn": "aws-cdk-lib.aws_route53.CfnHostedZone", "version": "0.0.0" } } }, "constructInfo": { - "fqn": "@aws-cdk/aws-route53.PublicHostedZone", + "fqn": "aws-cdk-lib.aws_route53.PublicHostedZone", + "version": "0.0.0" + } + }, + "Function": { + "id": "Function", + "path": "cdk-ses-email-identity-integ/Function", + "children": { + "ServiceRole": { + "id": "ServiceRole", + "path": "cdk-ses-email-identity-integ/Function/ServiceRole", + "children": { + "ImportServiceRole": { + "id": "ImportServiceRole", + "path": "cdk-ses-email-identity-integ/Function/ServiceRole/ImportServiceRole", + "constructInfo": { + "fqn": "aws-cdk-lib.Resource", + "version": "0.0.0" + } + }, + "Resource": { + "id": "Resource", + "path": "cdk-ses-email-identity-integ/Function/ServiceRole/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::IAM::Role", + "aws:cdk:cloudformation:props": { + "assumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "managedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_iam.CfnRole", + "version": "0.0.0" + } + }, + "DefaultPolicy": { + "id": "DefaultPolicy", + "path": "cdk-ses-email-identity-integ/Function/ServiceRole/DefaultPolicy", + "children": { + "Resource": { + "id": "Resource", + "path": "cdk-ses-email-identity-integ/Function/ServiceRole/DefaultPolicy/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::IAM::Policy", + "aws:cdk:cloudformation:props": { + "policyDocument": { + "Statement": [ + { + "Action": [ + "ses:SendEmail", + "ses:SendRawEmail" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":ses:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":identity/", + { + "Ref": "EmailIdentity7187767D" + } + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "policyName": "FunctionServiceRoleDefaultPolicy2F49994A", + "roles": [ + { + "Ref": "FunctionServiceRole675BB04A" + } + ] + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_iam.CfnPolicy", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_iam.Policy", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_iam.Role", + "version": "0.0.0" + } + }, + "Code": { + "id": "Code", + "path": "cdk-ses-email-identity-integ/Function/Code", + "children": { + "Stage": { + "id": "Stage", + "path": "cdk-ses-email-identity-integ/Function/Code/Stage", + "constructInfo": { + "fqn": "aws-cdk-lib.AssetStaging", + "version": "0.0.0" + } + }, + "AssetBucket": { + "id": "AssetBucket", + "path": "cdk-ses-email-identity-integ/Function/Code/AssetBucket", + "constructInfo": { + "fqn": "aws-cdk-lib.aws_s3.BucketBase", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_s3_assets.Asset", + "version": "0.0.0" + } + }, + "Resource": { + "id": "Resource", + "path": "cdk-ses-email-identity-integ/Function/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": "ab3156800c2f322f16da8bff913172139189a387dd64a4e622f82a790561fd4d.zip" + }, + "functionName": "email-sending-lambda", + "handler": "index.lambda_handler", + "role": { + "Fn::GetAtt": [ + "FunctionServiceRole675BB04A", + "Arn" + ] + }, + "runtime": "python3.11" + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_lambda.CfnFunction", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_lambda.Function", "version": "0.0.0" } }, @@ -50,16 +227,15 @@ "attributes": { "aws:cdk:cloudformation:type": "AWS::Route53::RecordSet", "aws:cdk:cloudformation:props": { + "hostedZoneId": { + "Ref": "HostedZoneDB99F866" + }, "name": { "Fn::GetAtt": [ "EmailIdentity7187767D", "DkimDNSTokenName1" ] }, - "type": "CNAME", - "hostedZoneId": { - "Ref": "HostedZoneDB99F866" - }, "resourceRecords": [ { "Fn::GetAtt": [ @@ -68,11 +244,12 @@ ] } ], - "ttl": "1800" + "ttl": "1800", + "type": "CNAME" } }, "constructInfo": { - "fqn": "@aws-cdk/aws-route53.CfnRecordSet", + "fqn": "aws-cdk-lib.aws_route53.CfnRecordSet", "version": "0.0.0" } }, @@ -82,16 +259,15 @@ "attributes": { "aws:cdk:cloudformation:type": "AWS::Route53::RecordSet", "aws:cdk:cloudformation:props": { + "hostedZoneId": { + "Ref": "HostedZoneDB99F866" + }, "name": { "Fn::GetAtt": [ "EmailIdentity7187767D", "DkimDNSTokenName2" ] }, - "type": "CNAME", - "hostedZoneId": { - "Ref": "HostedZoneDB99F866" - }, "resourceRecords": [ { "Fn::GetAtt": [ @@ -100,11 +276,12 @@ ] } ], - "ttl": "1800" + "ttl": "1800", + "type": "CNAME" } }, "constructInfo": { - "fqn": "@aws-cdk/aws-route53.CfnRecordSet", + "fqn": "aws-cdk-lib.aws_route53.CfnRecordSet", "version": "0.0.0" } }, @@ -114,16 +291,15 @@ "attributes": { "aws:cdk:cloudformation:type": "AWS::Route53::RecordSet", "aws:cdk:cloudformation:props": { + "hostedZoneId": { + "Ref": "HostedZoneDB99F866" + }, "name": { "Fn::GetAtt": [ "EmailIdentity7187767D", "DkimDNSTokenName3" ] }, - "type": "CNAME", - "hostedZoneId": { - "Ref": "HostedZoneDB99F866" - }, "resourceRecords": [ { "Fn::GetAtt": [ @@ -132,11 +308,12 @@ ] } ], - "ttl": "1800" + "ttl": "1800", + "type": "CNAME" } }, "constructInfo": { - "fqn": "@aws-cdk/aws-route53.CfnRecordSet", + "fqn": "aws-cdk-lib.aws_route53.CfnRecordSet", "version": "0.0.0" } }, @@ -153,7 +330,7 @@ } }, "constructInfo": { - "fqn": "@aws-cdk/aws-ses.CfnEmailIdentity", + "fqn": "aws-cdk-lib.aws_ses.CfnEmailIdentity", "version": "0.0.0" } }, @@ -167,11 +344,10 @@ "attributes": { "aws:cdk:cloudformation:type": "AWS::Route53::RecordSet", "aws:cdk:cloudformation:props": { - "name": "mail.cdk.dev.", - "type": "MX", "hostedZoneId": { "Ref": "HostedZoneDB99F866" }, + "name": "mail.cdk.dev.", "resourceRecords": [ { "Fn::Join": [ @@ -186,17 +362,18 @@ ] } ], - "ttl": "1800" + "ttl": "1800", + "type": "MX" } }, "constructInfo": { - "fqn": "@aws-cdk/aws-route53.CfnRecordSet", + "fqn": "aws-cdk-lib.aws_route53.CfnRecordSet", "version": "0.0.0" } } }, "constructInfo": { - "fqn": "@aws-cdk/aws-route53.MxRecord", + "fqn": "aws-cdk-lib.aws_route53.MxRecord", "version": "0.0.0" } }, @@ -210,38 +387,54 @@ "attributes": { "aws:cdk:cloudformation:type": "AWS::Route53::RecordSet", "aws:cdk:cloudformation:props": { - "name": "mail.cdk.dev.", - "type": "TXT", "hostedZoneId": { "Ref": "HostedZoneDB99F866" }, + "name": "mail.cdk.dev.", "resourceRecords": [ "\"v=spf1 include:amazonses.com ~all\"" ], - "ttl": "1800" + "ttl": "1800", + "type": "TXT" } }, "constructInfo": { - "fqn": "@aws-cdk/aws-route53.CfnRecordSet", + "fqn": "aws-cdk-lib.aws_route53.CfnRecordSet", "version": "0.0.0" } } }, "constructInfo": { - "fqn": "@aws-cdk/aws-route53.TxtRecord", + "fqn": "aws-cdk-lib.aws_route53.TxtRecord", "version": "0.0.0" } } }, "constructInfo": { - "fqn": "@aws-cdk/aws-ses.EmailIdentity", + "fqn": "aws-cdk-lib.aws_ses.EmailIdentity", + "version": "0.0.0" + } + }, + "BootstrapVersion": { + "id": "BootstrapVersion", + "path": "cdk-ses-email-identity-integ/BootstrapVersion", + "constructInfo": { + "fqn": "aws-cdk-lib.CfnParameter", + "version": "0.0.0" + } + }, + "CheckBootstrapVersion": { + "id": "CheckBootstrapVersion", + "path": "cdk-ses-email-identity-integ/CheckBootstrapVersion", + "constructInfo": { + "fqn": "aws-cdk-lib.CfnRule", "version": "0.0.0" } } }, "constructInfo": { - "fqn": "constructs.Construct", - "version": "10.1.85" + "fqn": "aws-cdk-lib.Stack", + "version": "0.0.0" } }, "EmailIdentityInteg": { @@ -257,33 +450,59 @@ "path": "EmailIdentityInteg/DefaultTest/Default", "constructInfo": { "fqn": "constructs.Construct", - "version": "10.1.85" + "version": "10.3.0" } }, "DeployAssert": { "id": "DeployAssert", "path": "EmailIdentityInteg/DefaultTest/DeployAssert", + "children": { + "BootstrapVersion": { + "id": "BootstrapVersion", + "path": "EmailIdentityInteg/DefaultTest/DeployAssert/BootstrapVersion", + "constructInfo": { + "fqn": "aws-cdk-lib.CfnParameter", + "version": "0.0.0" + } + }, + "CheckBootstrapVersion": { + "id": "CheckBootstrapVersion", + "path": "EmailIdentityInteg/DefaultTest/DeployAssert/CheckBootstrapVersion", + "constructInfo": { + "fqn": "aws-cdk-lib.CfnRule", + "version": "0.0.0" + } + } + }, "constructInfo": { - "fqn": "constructs.Construct", - "version": "10.1.85" + "fqn": "aws-cdk-lib.Stack", + "version": "0.0.0" } } }, "constructInfo": { - "fqn": "@aws-cdk/integ-tests.IntegTestCase", + "fqn": "@aws-cdk/integ-tests-alpha.IntegTestCase", "version": "0.0.0" } } }, "constructInfo": { - "fqn": "@aws-cdk/integ-tests.IntegTest", + "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": "constructs.Construct", - "version": "10.1.85" + "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-ses/test/integ.email-identity.ts b/packages/@aws-cdk-testing/framework-integ/test/aws-ses/test/integ.email-identity.ts index 5a0bd64bbff69..20888683f53dc 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-ses/test/integ.email-identity.ts +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-ses/test/integ.email-identity.ts @@ -3,6 +3,8 @@ import { App, Stack, StackProps } from 'aws-cdk-lib'; import * as integ from '@aws-cdk/integ-tests-alpha'; import { Construct } from 'constructs'; import * as ses from 'aws-cdk-lib/aws-ses'; +import { Code, Function, Runtime } from 'aws-cdk-lib/aws-lambda'; +import * as path from 'path'; class TestStack extends Stack { constructor(scope: Construct, id: string, props?: StackProps) { @@ -12,10 +14,19 @@ class TestStack extends Stack { zoneName: 'cdk.dev', }); - new ses.EmailIdentity(this, 'EmailIdentity', { + const lambdaFunction = new Function(this, 'Function', { + functionName: 'email-sending-lambda', + runtime: Runtime.PYTHON_3_11, + code: Code.fromAsset(path.join(__dirname, 'fixtures', 'send-email')), + handler: 'index.lambda_handler', + }); + + const emailIdentity = new ses.EmailIdentity(this, 'EmailIdentity', { identity: ses.Identity.publicHostedZone(hostedZone), mailFromDomain: 'mail.cdk.dev', }); + + emailIdentity.grantSendEmail(lambdaFunction); } } diff --git a/packages/aws-cdk-lib/aws-ses/README.md b/packages/aws-cdk-lib/aws-ses/README.md index 5120aedd90bc8..eb59f3cc6bc9c 100644 --- a/packages/aws-cdk-lib/aws-ses/README.md +++ b/packages/aws-cdk-lib/aws-ses/README.md @@ -210,6 +210,22 @@ for (const record of identity.dkimRecords) { } ``` +#### Grants + +To grant a specific action to a principal use the `grant` method. +For sending emails, `grantSendEmail` can be used instead: + +```ts +import * as iam from 'aws-cdk-lib/aws-iam'; +declare const user: iam.User; + +const identity = new ses.EmailIdentity(this, 'Identity', { + identity: ses.Identity.domain('cdk.dev'), +}); + +identity.grantSendEmail(user); +``` + ### Virtual Deliverability Manager (VDM) Virtual Deliverability Manager is an Amazon SES feature that helps you enhance email deliverability, diff --git a/packages/aws-cdk-lib/aws-ses/lib/email-identity.ts b/packages/aws-cdk-lib/aws-ses/lib/email-identity.ts index f7282af644ef9..49ec4298d17c1 100644 --- a/packages/aws-cdk-lib/aws-ses/lib/email-identity.ts +++ b/packages/aws-cdk-lib/aws-ses/lib/email-identity.ts @@ -2,6 +2,7 @@ import { Construct } from 'constructs'; import { IConfigurationSet } from './configuration-set'; import { undefinedIfNoKeys } from './private/utils'; import { CfnEmailIdentity } from './ses.generated'; +import { Grant, IGrantable } from '../../aws-iam'; import { IPublicHostedZone } from '../../aws-route53'; import * as route53 from '../../aws-route53'; import { IResource, Lazy, Resource, SecretValue, Stack } from '../../core'; @@ -16,6 +17,30 @@ export interface IEmailIdentity extends IResource { * @attribute */ readonly emailIdentityName: string; + + /** + * The ARN of the email identity + * + * @attribute + */ + readonly emailIdentityArn: string; + + /** + * Adds an IAM policy statement associated with this email identity to an IAM principal's policy. + * + * @param grantee the principal (no-op if undefined) + * @param actions the set of actions to allow + */ + grant(grantee: IGrantable, ...actions: string[]): Grant; + + /** + * Permits an IAM principal the send email action. + * + * Actions: SendEmail. + * + * @param grantee the principal to grant access to + */ + grantSendEmail(grantee: IGrantable): Grant; } /** @@ -310,22 +335,73 @@ export enum EasyDkimSigningKeyLength { RSA_2048_BIT = 'RSA_2048_BIT', } +abstract class EmailIdentityBase extends Resource implements IEmailIdentity { + /** + * The name of the email identity + * + * @attribute + */ + public abstract readonly emailIdentityName: string; + + /** + * The ARN of the email identity + * + * @attribute + */ + public abstract readonly emailIdentityArn: string; + + /** + * Adds an IAM policy statement associated with this email identity to an IAM principal's policy. + * + * @param grantee the principal (no-op if undefined) + * @param actions the set of actions to allow + */ + public grant(grantee: IGrantable, ...actions: string[]): Grant { + const resourceArns = [this.emailIdentityArn]; + return Grant.addToPrincipal({ + grantee, + actions, + resourceArns, + scope: this, + }); + } + + /** + * Permits an IAM principal the send email action. + * + * Actions: SendEmail, SendRawEmail. + * + * @param grantee the principal to grant access to + */ + public grantSendEmail(grantee: IGrantable): Grant { + return this.grant(grantee, 'ses:SendEmail', 'ses:SendRawEmail'); + } +} + /** * An email identity */ -export class EmailIdentity extends Resource implements IEmailIdentity { +export class EmailIdentity extends EmailIdentityBase { /** * Use an existing email identity */ public static fromEmailIdentityName(scope: Construct, id: string, emailIdentityName: string): IEmailIdentity { - class Import extends Resource implements IEmailIdentity { + class Import extends EmailIdentityBase { public readonly emailIdentityName = emailIdentityName; + + public readonly emailIdentityArn = this.stack.formatArn({ + service: 'ses', + resource: 'identity', + resourceName: this.emailIdentityName, + }); } return new Import(scope, id); } public readonly emailIdentityName: string; + public readonly emailIdentityArn: string; + /** * The host name for the first token that you have to add to the * DNS configurationfor your domain @@ -421,6 +497,12 @@ export class EmailIdentity extends Resource implements IEmailIdentity { this.emailIdentityName = identity.ref; + this.emailIdentityArn = this.stack.formatArn({ + service: 'ses', + resource: 'identity', + resourceName: this.emailIdentityName, + }); + this.dkimDnsTokenName1 = identity.attrDkimDnsTokenName1; this.dkimDnsTokenName2 = identity.attrDkimDnsTokenName2; this.dkimDnsTokenName3 = identity.attrDkimDnsTokenName3; diff --git a/packages/aws-cdk-lib/aws-ses/test/email-identity.test.ts b/packages/aws-cdk-lib/aws-ses/test/email-identity.test.ts index adf121a4586b5..29cff96a57410 100644 --- a/packages/aws-cdk-lib/aws-ses/test/email-identity.test.ts +++ b/packages/aws-cdk-lib/aws-ses/test/email-identity.test.ts @@ -1,4 +1,5 @@ import { Template } from '../../assertions'; +import { User } from '../../aws-iam'; import * as route53 from '../../aws-route53'; import { SecretValue, Stack } from '../../core'; import { ConfigurationSet, DkimIdentity, EmailIdentity, Identity, MailFromBehaviorOnMxFailure } from '../lib'; @@ -191,3 +192,138 @@ test('with mail from and hosted zone', () => { }); }); +describe('grants', () => { + test('grant on a domain identity', () => { + // GIVEN + stack = new Stack(undefined, 'Stack', { env: { region: 'us-west-2', account: '123456789012' } }); + const user = new User(stack, 'User'); + const emailIdentity = new EmailIdentity(stack, 'Identity', { + identity: Identity.domain('cdk.dev'), + }); + + // WHEN + emailIdentity.grant(user, 'ses:action1', 'ses:action2'); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: ['ses:action1', 'ses:action2'], + Effect: 'Allow', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':ses:us-west-2:123456789012:identity/', + { Ref: 'Identity2D60E2CC' }, + ], + ], + }, + }, + ], + Version: '2012-10-17', + }, + PolicyName: 'UserDefaultPolicy1F97781E', + Users: [ + { + Ref: 'User00B015A1', + }, + ], + }); + }); + + test('grant on an email identity from name', () => { + // GIVEN + stack = new Stack(undefined, 'Stack', { env: { region: 'us-west-2', account: '123456789012' } }); + const user = new User(stack, 'User'); + const emailIdentity = EmailIdentity.fromEmailIdentityName( + stack, + 'Identity', + 'cdk.dev', + ); + + // WHEN + emailIdentity.grant(user, 'ses:action1', 'ses:action2'); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: [ + 'ses:action1', + 'ses:action2', + ], + Effect: 'Allow', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':ses:us-west-2:123456789012:identity/cdk.dev', + ], + ], + }, + }, + ], + Version: '2012-10-17', + }, + PolicyName: 'UserDefaultPolicy1F97781E', + Users: [ + { + Ref: 'User00B015A1', + }, + ], + }); + }); + + test('grantSendEmail', () => { + // GIVEN + stack = new Stack(undefined, 'Stack', { env: { region: 'us-west-2', account: '123456789012' } }); + const user = new User(stack, 'User'); + const emailIdentity = EmailIdentity.fromEmailIdentityName( + stack, + 'Identity', + 'cdk.dev', + ); + + // WHEN + emailIdentity.grantSendEmail(user); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: [ + 'ses:SendEmail', + 'ses:SendRawEmail', + ], + Effect: 'Allow', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':ses:us-west-2:123456789012:identity/cdk.dev', + ], + ], + }, + }, + ], + Version: '2012-10-17', + }, + PolicyName: 'UserDefaultPolicy1F97781E', + Users: [ + { + Ref: 'User00B015A1', + }, + ], + }); + }); +});