diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns.js.snapshot/SNSInteg.assets.json b/packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns.js.snapshot/SNSInteg.assets.json index fd178000c1916..61c051ccce17a 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns.js.snapshot/SNSInteg.assets.json +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns.js.snapshot/SNSInteg.assets.json @@ -1,7 +1,7 @@ { - "version": "36.0.0", + "version": "39.0.0", "files": { - "f73578a6268bf972ab72b4905cc09579fd59ece18de3cd0622ff9606ebdcf661": { + "c2cf07de7fe74e18626ea2af3fe2b5f6a5d717a3ee5e4ac96b694e6d2511c68b": { "source": { "path": "SNSInteg.template.json", "packaging": "file" @@ -9,7 +9,7 @@ "destinations": { "current_account-current_region": { "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", - "objectKey": "f73578a6268bf972ab72b4905cc09579fd59ece18de3cd0622ff9606ebdcf661.json", + "objectKey": "c2cf07de7fe74e18626ea2af3fe2b5f6a5d717a3ee5e4ac96b694e6d2511c68b.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-sns/test/integ.sns.js.snapshot/SNSInteg.template.json b/packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns.js.snapshot/SNSInteg.template.json index 6b67cc458f25f..1d3bb50ec79fa 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns.js.snapshot/SNSInteg.template.json +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns.js.snapshot/SNSInteg.template.json @@ -27,6 +27,17 @@ } }, "Resource": "*" + }, + { + "Action": [ + "kms:Decrypt", + "kms:GenerateDataKey*" + ], + "Effect": "Allow", + "Principal": { + "Service": "s3.amazonaws.com" + }, + "Resource": "*" } ], "Version": "2012-10-17" @@ -132,12 +143,6 @@ "Type": "AWS::SNS::Topic", "Properties": { "DisplayName": "fooDisplayName2", - "KmsMasterKeyId": { - "Fn::GetAtt": [ - "CustomKey1E6D0D07", - "Arn" - ] - }, "TopicName": "fooTopic2" } }, @@ -166,8 +171,26 @@ { "Action": "sns:Publish", "Effect": "Allow", + "Resource": [ + { + "Ref": "MyTopic288CE2107" + }, + { + "Ref": "MyTopic3134CFDFB" + } + ] + }, + { + "Action": [ + "kms:Decrypt", + "kms:GenerateDataKey*" + ], + "Effect": "Allow", "Resource": { - "Ref": "MyTopic288CE2107" + "Fn::GetAtt": [ + "CustomKey1E6D0D07", + "Arn" + ] } } ], @@ -180,6 +203,19 @@ } ] } + }, + "MyTopic3134CFDFB": { + "Type": "AWS::SNS::Topic", + "Properties": { + "DisplayName": "fooDisplayName3", + "KmsMasterKeyId": { + "Fn::GetAtt": [ + "CustomKey1E6D0D07", + "Arn" + ] + }, + "TopicName": "fooTopic3" + } } }, "Parameters": { diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns.js.snapshot/cdk.out b/packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns.js.snapshot/cdk.out index 1f0068d32659a..91e1a8b9901d5 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns.js.snapshot/cdk.out +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns.js.snapshot/cdk.out @@ -1 +1 @@ -{"version":"36.0.0"} \ No newline at end of file +{"version":"39.0.0"} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns.js.snapshot/integ.json b/packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns.js.snapshot/integ.json index 72d9cc0b958b9..9fc70f8fafa11 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns.js.snapshot/integ.json +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns.js.snapshot/integ.json @@ -1,5 +1,5 @@ { - "version": "36.0.0", + "version": "39.0.0", "testCases": { "integ.sns": { "stacks": [ diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns.js.snapshot/manifest.json b/packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns.js.snapshot/manifest.json index f3ce32f94ee6f..bac2f790aef9a 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns.js.snapshot/manifest.json +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns.js.snapshot/manifest.json @@ -1,5 +1,5 @@ { - "version": "36.0.0", + "version": "39.0.0", "artifacts": { "SNSInteg.assets": { "type": "cdk:asset-manifest", @@ -18,7 +18,7 @@ "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}/f73578a6268bf972ab72b4905cc09579fd59ece18de3cd0622ff9606ebdcf661.json", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/c2cf07de7fe74e18626ea2af3fe2b5f6a5d717a3ee5e4ac96b694e6d2511c68b.json", "requiresBootstrapStackVersion": 6, "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", "additionalDependencies": [ @@ -88,6 +88,12 @@ "data": "PublishRoleDefaultPolicy9257B12D" } ], + "/SNSInteg/MyTopic3/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "MyTopic3134CFDFB" + } + ], "/SNSInteg/BootstrapVersion": [ { "type": "aws:cdk:logicalId", diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns.js.snapshot/tree.json b/packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns.js.snapshot/tree.json index 752d655b08ecb..51171f4584cee 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns.js.snapshot/tree.json +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns.js.snapshot/tree.json @@ -42,6 +42,17 @@ } }, "Resource": "*" + }, + { + "Action": [ + "kms:Decrypt", + "kms:GenerateDataKey*" + ], + "Effect": "Allow", + "Principal": { + "Service": "s3.amazonaws.com" + }, + "Resource": "*" } ], "Version": "2012-10-17" @@ -269,12 +280,6 @@ "aws:cdk:cloudformation:type": "AWS::SNS::Topic", "aws:cdk:cloudformation:props": { "displayName": "fooDisplayName2", - "kmsMasterKeyId": { - "Fn::GetAtt": [ - "CustomKey1E6D0D07", - "Arn" - ] - }, "topicName": "fooTopic2" } }, @@ -289,9 +294,9 @@ "version": "0.0.0" } }, - "ImportedTopic": { - "id": "ImportedTopic", - "path": "SNSInteg/ImportedTopic", + "ImportedTopic2": { + "id": "ImportedTopic2", + "path": "SNSInteg/ImportedTopic2", "constructInfo": { "fqn": "aws-cdk-lib.aws_sns.TopicBase", "version": "0.0.0" @@ -349,8 +354,28 @@ { "Action": "sns:Publish", "Effect": "Allow", + "Resource": [ + { + "Ref": "MyTopic288CE2107" + }, + { + "Ref": "MyTopic3134CFDFB" + } + ] + }, + { + "Action": [ + "kms:Decrypt", + "kms:Encrypt", + "kms:GenerateDataKey*", + "kms:ReEncrypt*" + ], + "Effect": "Allow", "Resource": { - "Ref": "MyTopic288CE2107" + "Fn::GetAtt": [ + "CustomKey1E6D0D07", + "Arn" + ] } } ], @@ -381,6 +406,55 @@ "version": "0.0.0" } }, + "MyTopic3": { + "id": "MyTopic3", + "path": "SNSInteg/MyTopic3", + "children": { + "Resource": { + "id": "Resource", + "path": "SNSInteg/MyTopic3/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::SNS::Topic", + "aws:cdk:cloudformation:props": { + "displayName": "fooDisplayName3", + "kmsMasterKeyId": { + "Fn::GetAtt": [ + "CustomKey1E6D0D07", + "Arn" + ] + }, + "topicName": "fooTopic3" + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_sns.CfnTopic", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_sns.Topic", + "version": "0.0.0" + } + }, + "ImportedTopic3": { + "id": "ImportedTopic3", + "path": "SNSInteg/ImportedTopic3", + "children": { + "Key": { + "id": "Key", + "path": "SNSInteg/ImportedTopic3/Key", + "constructInfo": { + "fqn": "aws-cdk-lib.Resource", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_sns.TopicBase", + "version": "0.0.0" + } + }, "BootstrapVersion": { "id": "BootstrapVersion", "path": "SNSInteg/BootstrapVersion", @@ -408,7 +482,7 @@ "path": "Tree", "constructInfo": { "fqn": "constructs.Construct", - "version": "10.3.0" + "version": "10.4.2" } } }, diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns.ts b/packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns.ts index 3a24d4e855428..fab5bf9da88e3 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns.ts +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns.ts @@ -62,14 +62,32 @@ class SNSInteg extends Stack { const topic2 = new Topic(this, 'MyTopic2', { topicName: 'fooTopic2', displayName: 'fooDisplayName2', - masterKey: key, }); - const importedTopic = Topic.fromTopicArn(this, 'ImportedTopic', topic2.topicArn); + const importedTopic2 = Topic.fromTopicArn(this, 'ImportedTopic2', topic2.topicArn); const publishRole = new Role(this, 'PublishRole', { assumedBy: new ServicePrincipal('s3.amazonaws.com'), }); - importedTopic.grantPublish(publishRole); + importedTopic2.grantPublish(publishRole); + + // Can import encrypted topic by attributes + const topic3 = new Topic(this, 'MyTopic3', { + topicName: 'fooTopic3', + displayName: 'fooDisplayName3', + masterKey: key, + }); + const importedTopic3 = Topic.fromTopicAttributes(this, 'ImportedTopic3', { + topicArn: topic3.topicArn, + keyArn: key.keyArn, + }); + importedTopic3.grantPublish(publishRole); + + // Can reference KMS key after creation + topic3.masterKey!.addToResourcePolicy(new PolicyStatement({ + principals: [new ServicePrincipal('s3.amazonaws.com')], + actions: ['kms:GenerateDataKey*', 'kms:Decrypt'], + resources: ['*'], + })); } } diff --git a/packages/aws-cdk-lib/aws-sns/lib/topic-base.ts b/packages/aws-cdk-lib/aws-sns/lib/topic-base.ts index 4a50c87c570bb..2dbe20ffd713e 100644 --- a/packages/aws-cdk-lib/aws-sns/lib/topic-base.ts +++ b/packages/aws-cdk-lib/aws-sns/lib/topic-base.ts @@ -5,6 +5,7 @@ import { ITopicSubscription } from './subscriber'; import { Subscription } from './subscription'; import * as notifications from '../../aws-codestarnotifications'; import * as iam from '../../aws-iam'; +import { IKey } from '../../aws-kms'; import { IResource, Resource, ResourceProps, Token } from '../../core'; /** @@ -25,6 +26,17 @@ export interface ITopic extends IResource, notifications.INotificationRuleTarget */ readonly topicName: string; + /** + * A KMS Key, either managed by this CDK app, or imported. + * + * This property applies only to server-side encryption. + * + * @see https://docs.aws.amazon.com/sns/latest/dg/sns-server-side-encryption.html + * + * @default None + */ + readonly masterKey?: IKey; + /** * Enables content-based deduplication for FIFO topics. * @@ -72,6 +84,8 @@ export abstract class TopicBase extends Resource implements ITopic { public abstract readonly topicName: string; + public abstract readonly masterKey?: IKey; + public abstract readonly fifo: boolean; public abstract readonly contentBasedDeduplication: boolean; @@ -173,12 +187,16 @@ export abstract class TopicBase extends Resource implements ITopic { * Grant topic publishing permissions to the given identity */ public grantPublish(grantee: iam.IGrantable) { - return iam.Grant.addToPrincipalOrResource({ + const ret = iam.Grant.addToPrincipalOrResource({ grantee, actions: ['sns:Publish'], resourceArns: [this.topicArn], resource: this, }); + if (this.masterKey) { + this.masterKey.grant(grantee, 'kms:Decrypt', 'kms:GenerateDataKey*'); + } + return ret; } /** diff --git a/packages/aws-cdk-lib/aws-sns/lib/topic.ts b/packages/aws-cdk-lib/aws-sns/lib/topic.ts index f78b32bf8cfe3..98a74d47d1953 100644 --- a/packages/aws-cdk-lib/aws-sns/lib/topic.ts +++ b/packages/aws-cdk-lib/aws-sns/lib/topic.ts @@ -2,7 +2,7 @@ import { Construct } from 'constructs'; import { CfnTopic } from './sns.generated'; import { ITopic, TopicBase } from './topic-base'; import { IRole } from '../../aws-iam'; -import { IKey } from '../../aws-kms'; +import { IKey, Key } from '../../aws-kms'; import { ArnFormat, Lazy, Names, Stack, Token } from '../../core'; /** @@ -189,6 +189,13 @@ export interface TopicAttributes { */ readonly topicArn: string; + /** + * KMS encryption key, if this topic is server-side encrypted by a KMS key. + * + * @default - None + */ + readonly keyArn?: string; + /** * Whether content-based deduplication is enabled. * Only applicable for FIFO topics. @@ -232,6 +239,9 @@ export class Topic extends TopicBase { class Import extends TopicBase { public readonly topicArn = attrs.topicArn; public readonly topicName = topicName; + public readonly masterKey = attrs.keyArn + ? Key.fromKeyArn(this, 'Key', attrs.keyArn) + : undefined; public readonly fifo = fifo; public readonly contentBasedDeduplication = attrs.contentBasedDeduplication || false; protected autoCreatePolicy: boolean = false; @@ -244,6 +254,7 @@ export class Topic extends TopicBase { public readonly topicArn: string; public readonly topicName: string; + public readonly masterKey?: IKey; public readonly contentBasedDeduplication: boolean; public readonly fifo: boolean; @@ -322,6 +333,7 @@ export class Topic extends TopicBase { resource: this.physicalName, }); this.topicName = this.getResourceNameAttribute(resource.attrTopicName); + this.masterKey = props.masterKey; this.fifo = props.fifo || false; this.contentBasedDeduplication = props.contentBasedDeduplication || false; } diff --git a/packages/aws-cdk-lib/aws-sns/test/sns.test.ts b/packages/aws-cdk-lib/aws-sns/test/sns.test.ts index 6c8f04fb096b6..e41608c65f380 100644 --- a/packages/aws-cdk-lib/aws-sns/test/sns.test.ts +++ b/packages/aws-cdk-lib/aws-sns/test/sns.test.ts @@ -283,7 +283,38 @@ describe('Topic', () => { ], }, }); + }); + + test('give publishing permissions with masterKey', () => { + // GIVEN + const stack = new cdk.Stack(); + const key = new kms.Key(stack, 'CustomKey'); + const topic = new sns.Topic(stack, 'Topic', { masterKey: key }); + const user = new iam.User(stack, 'User'); + // WHEN + topic.grantPublish(user); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { + 'PolicyDocument': { + Version: '2012-10-17', + 'Statement': [ + { + 'Action': 'sns:Publish', + 'Effect': 'Allow', + 'Resource': stack.resolve(topic.topicArn), + }, + { + 'Action': ['kms:Decrypt', 'kms:GenerateDataKey*'], + 'Effect': 'Allow', + 'Resource': { + 'Fn::GetAtt': ['CustomKey1E6D0D07', 'Arn'], + }, + }, + ], + }, + }); }); test('give subscribing permissions', () => { @@ -532,6 +563,21 @@ describe('Topic', () => { })).toThrow(/Cannot import topic; contentBasedDeduplication is only available for FIFO SNS topics./); }); + test('fromTopicAttributes keyArn', () => { + // GIVEN + const stack = new cdk.Stack(); + const keyArn = 'arn:aws:kms:us-east-1:123456789012:key/1234abcd-12ab-34cd-56ef-1234567890ab'; + + //WHEN + const imported = sns.Topic.fromTopicAttributes(stack, 'Imported', { + topicArn: 'arn:aws:sns:*:123456789012:mytopic', + keyArn, + }); + + // THEN + expect(imported.masterKey?.keyArn).toEqual(keyArn); + }); + test('sets account for imported topic env', () => { // GIVEN const stack = new cdk.Stack();