From 36a48adb8cf9ecfa21fbdee4d61baee16391f07a Mon Sep 17 00:00:00 2001 From: mazyu36 Date: Tue, 11 Jun 2024 02:07:28 +0900 Subject: [PATCH] feat(sns): add grantSubscribe method (#30486) ### Issue # (if applicable) Closes #29049. ### Reason for this change Allow the Topic construct to expose a method to grant subscription permissions to a grantable resource. It's useful when you want to allow entities, such as another AWS account or resources created later, to subscribe to the topic at their own pace, separating permission granting from the actual subscription process. ### Description of changes Add grantSubscribe method to ITopic interface and TopicBase class. ### Description of how you validated changes Add unit tests and integ tests. ### 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* --- .../cdk.out | 1 + .../integ.json | 12 + .../manifest.json | 158 +++++++++++ .../sns-grant-subscribe-stack.assets.json | 19 ++ .../sns-grant-subscribe-stack.template.json | 111 ++++++++ ...efaultTestDeployAssertE3ABCE3F.assets.json | 19 ++ ...aultTestDeployAssertE3ABCE3F.template.json | 36 +++ .../tree.json | 245 ++++++++++++++++++ .../test/integ.sns-topic-grant-subscribe.ts | 35 +++ packages/aws-cdk-lib/aws-sns/README.md | 14 +- .../aws-cdk-lib/aws-sns/lib/topic-base.ts | 19 +- packages/aws-cdk-lib/aws-sns/test/sns.test.ts | 25 ++ 12 files changed, 691 insertions(+), 3 deletions(-) create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns-topic-grant-subscribe.js.snapshot/cdk.out create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns-topic-grant-subscribe.js.snapshot/integ.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns-topic-grant-subscribe.js.snapshot/manifest.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns-topic-grant-subscribe.js.snapshot/sns-grant-subscribe-stack.assets.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns-topic-grant-subscribe.js.snapshot/sns-grant-subscribe-stack.template.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns-topic-grant-subscribe.js.snapshot/snsgrantsubscribetestDefaultTestDeployAssertE3ABCE3F.assets.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns-topic-grant-subscribe.js.snapshot/snsgrantsubscribetestDefaultTestDeployAssertE3ABCE3F.template.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns-topic-grant-subscribe.js.snapshot/tree.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns-topic-grant-subscribe.ts diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns-topic-grant-subscribe.js.snapshot/cdk.out b/packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns-topic-grant-subscribe.js.snapshot/cdk.out new file mode 100644 index 0000000000000..1f0068d32659a --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns-topic-grant-subscribe.js.snapshot/cdk.out @@ -0,0 +1 @@ +{"version":"36.0.0"} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns-topic-grant-subscribe.js.snapshot/integ.json b/packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns-topic-grant-subscribe.js.snapshot/integ.json new file mode 100644 index 0000000000000..2ca0634b8bbb0 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns-topic-grant-subscribe.js.snapshot/integ.json @@ -0,0 +1,12 @@ +{ + "version": "36.0.0", + "testCases": { + "sns-grant-subscribe-test/DefaultTest": { + "stacks": [ + "sns-grant-subscribe-stack" + ], + "assertionStack": "sns-grant-subscribe-test/DefaultTest/DeployAssert", + "assertionStackName": "snsgrantsubscribetestDefaultTestDeployAssertE3ABCE3F" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns-topic-grant-subscribe.js.snapshot/manifest.json b/packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns-topic-grant-subscribe.js.snapshot/manifest.json new file mode 100644 index 0000000000000..b6a0b3c07b5bf --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns-topic-grant-subscribe.js.snapshot/manifest.json @@ -0,0 +1,158 @@ +{ + "version": "36.0.0", + "artifacts": { + "sns-grant-subscribe-stack.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "sns-grant-subscribe-stack.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "sns-grant-subscribe-stack": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "sns-grant-subscribe-stack.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}/0ed80096347e21be0423668860baa6a164892ffdec483fca2e4683573fe001df.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", + "additionalDependencies": [ + "sns-grant-subscribe-stack.assets" + ], + "lookupRole": { + "arn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}", + "requiresBootstrapStackVersion": 8, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "dependencies": [ + "sns-grant-subscribe-stack.assets" + ], + "metadata": { + "/sns-grant-subscribe-stack/CustomKey/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "CustomKey1E6D0D07" + } + ], + "/sns-grant-subscribe-stack/MyTopic/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "MyTopic86869434" + } + ], + "/sns-grant-subscribe-stack/MyUser/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "MyUserDC45028B" + } + ], + "/sns-grant-subscribe-stack/MyUser/DefaultPolicy/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "MyUserDefaultPolicy7B897426" + } + ], + "/sns-grant-subscribe-stack/BootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "BootstrapVersion" + } + ], + "/sns-grant-subscribe-stack/CheckBootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "CheckBootstrapVersion" + } + ], + "MyFuncServiceRole54065130": [ + { + "type": "aws:cdk:logicalId", + "data": "MyFuncServiceRole54065130", + "trace": [ + "!!DESTRUCTIVE_CHANGES: WILL_DESTROY" + ] + } + ], + "MyFuncServiceRoleDefaultPolicyF3C36699": [ + { + "type": "aws:cdk:logicalId", + "data": "MyFuncServiceRoleDefaultPolicyF3C36699", + "trace": [ + "!!DESTRUCTIVE_CHANGES: WILL_DESTROY" + ] + } + ], + "MyFunc8A243A2C": [ + { + "type": "aws:cdk:logicalId", + "data": "MyFunc8A243A2C", + "trace": [ + "!!DESTRUCTIVE_CHANGES: WILL_DESTROY" + ] + } + ] + }, + "displayName": "sns-grant-subscribe-stack" + }, + "snsgrantsubscribetestDefaultTestDeployAssertE3ABCE3F.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "snsgrantsubscribetestDefaultTestDeployAssertE3ABCE3F.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "snsgrantsubscribetestDefaultTestDeployAssertE3ABCE3F": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "snsgrantsubscribetestDefaultTestDeployAssertE3ABCE3F.template.json", + "terminationProtection": false, + "validateOnSynth": false, + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", + "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", + "additionalDependencies": [ + "snsgrantsubscribetestDefaultTestDeployAssertE3ABCE3F.assets" + ], + "lookupRole": { + "arn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}", + "requiresBootstrapStackVersion": 8, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "dependencies": [ + "snsgrantsubscribetestDefaultTestDeployAssertE3ABCE3F.assets" + ], + "metadata": { + "/sns-grant-subscribe-test/DefaultTest/DeployAssert/BootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "BootstrapVersion" + } + ], + "/sns-grant-subscribe-test/DefaultTest/DeployAssert/CheckBootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "CheckBootstrapVersion" + } + ] + }, + "displayName": "sns-grant-subscribe-test/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-sns/test/integ.sns-topic-grant-subscribe.js.snapshot/sns-grant-subscribe-stack.assets.json b/packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns-topic-grant-subscribe.js.snapshot/sns-grant-subscribe-stack.assets.json new file mode 100644 index 0000000000000..a27200a8c9580 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns-topic-grant-subscribe.js.snapshot/sns-grant-subscribe-stack.assets.json @@ -0,0 +1,19 @@ +{ + "version": "36.0.0", + "files": { + "0ed80096347e21be0423668860baa6a164892ffdec483fca2e4683573fe001df": { + "source": { + "path": "sns-grant-subscribe-stack.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "0ed80096347e21be0423668860baa6a164892ffdec483fca2e4683573fe001df.json", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + } + }, + "dockerImages": {} +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns-topic-grant-subscribe.js.snapshot/sns-grant-subscribe-stack.template.json b/packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns-topic-grant-subscribe.js.snapshot/sns-grant-subscribe-stack.template.json new file mode 100644 index 0000000000000..ae9d59c48a93c --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns-topic-grant-subscribe.js.snapshot/sns-grant-subscribe-stack.template.json @@ -0,0 +1,111 @@ +{ + "Resources": { + "CustomKey1E6D0D07": { + "Type": "AWS::KMS::Key", + "Properties": { + "KeyPolicy": { + "Statement": [ + { + "Action": "kms:*", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":root" + ] + ] + } + }, + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "PendingWindowInDays": 7 + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "MyTopic86869434": { + "Type": "AWS::SNS::Topic", + "Properties": { + "KmsMasterKeyId": { + "Fn::GetAtt": [ + "CustomKey1E6D0D07", + "Arn" + ] + } + } + }, + "MyUserDC45028B": { + "Type": "AWS::IAM::User" + }, + "MyUserDefaultPolicy7B897426": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "sns:Subscribe", + "Effect": "Allow", + "Resource": { + "Ref": "MyTopic86869434" + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "MyUserDefaultPolicy7B897426", + "Users": [ + { + "Ref": "MyUserDC45028B" + } + ] + } + } + }, + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns-topic-grant-subscribe.js.snapshot/snsgrantsubscribetestDefaultTestDeployAssertE3ABCE3F.assets.json b/packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns-topic-grant-subscribe.js.snapshot/snsgrantsubscribetestDefaultTestDeployAssertE3ABCE3F.assets.json new file mode 100644 index 0000000000000..fb2727828556f --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns-topic-grant-subscribe.js.snapshot/snsgrantsubscribetestDefaultTestDeployAssertE3ABCE3F.assets.json @@ -0,0 +1,19 @@ +{ + "version": "36.0.0", + "files": { + "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22": { + "source": { + "path": "snsgrantsubscribetestDefaultTestDeployAssertE3ABCE3F.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22.json", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + } + }, + "dockerImages": {} +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns-topic-grant-subscribe.js.snapshot/snsgrantsubscribetestDefaultTestDeployAssertE3ABCE3F.template.json b/packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns-topic-grant-subscribe.js.snapshot/snsgrantsubscribetestDefaultTestDeployAssertE3ABCE3F.template.json new file mode 100644 index 0000000000000..ad9d0fb73d1dd --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns-topic-grant-subscribe.js.snapshot/snsgrantsubscribetestDefaultTestDeployAssertE3ABCE3F.template.json @@ -0,0 +1,36 @@ +{ + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns-topic-grant-subscribe.js.snapshot/tree.json b/packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns-topic-grant-subscribe.js.snapshot/tree.json new file mode 100644 index 0000000000000..caac9048238c4 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns-topic-grant-subscribe.js.snapshot/tree.json @@ -0,0 +1,245 @@ +{ + "version": "tree-0.1", + "tree": { + "id": "App", + "path": "", + "children": { + "sns-grant-subscribe-stack": { + "id": "sns-grant-subscribe-stack", + "path": "sns-grant-subscribe-stack", + "children": { + "CustomKey": { + "id": "CustomKey", + "path": "sns-grant-subscribe-stack/CustomKey", + "children": { + "Resource": { + "id": "Resource", + "path": "sns-grant-subscribe-stack/CustomKey/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::KMS::Key", + "aws:cdk:cloudformation:props": { + "keyPolicy": { + "Statement": [ + { + "Action": "kms:*", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":root" + ] + ] + } + }, + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "pendingWindowInDays": 7 + } + }, + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + } + }, + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + }, + "MyTopic": { + "id": "MyTopic", + "path": "sns-grant-subscribe-stack/MyTopic", + "children": { + "Resource": { + "id": "Resource", + "path": "sns-grant-subscribe-stack/MyTopic/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::SNS::Topic", + "aws:cdk:cloudformation:props": { + "kmsMasterKeyId": { + "Fn::GetAtt": [ + "CustomKey1E6D0D07", + "Arn" + ] + } + } + }, + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + } + }, + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + }, + "MyUser": { + "id": "MyUser", + "path": "sns-grant-subscribe-stack/MyUser", + "children": { + "Resource": { + "id": "Resource", + "path": "sns-grant-subscribe-stack/MyUser/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::IAM::User", + "aws:cdk:cloudformation:props": {} + }, + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + }, + "DefaultPolicy": { + "id": "DefaultPolicy", + "path": "sns-grant-subscribe-stack/MyUser/DefaultPolicy", + "children": { + "Resource": { + "id": "Resource", + "path": "sns-grant-subscribe-stack/MyUser/DefaultPolicy/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::IAM::Policy", + "aws:cdk:cloudformation:props": { + "policyDocument": { + "Statement": [ + { + "Action": "sns:Subscribe", + "Effect": "Allow", + "Resource": { + "Ref": "MyTopic86869434" + } + } + ], + "Version": "2012-10-17" + }, + "policyName": "MyUserDefaultPolicy7B897426", + "users": [ + { + "Ref": "MyUserDC45028B" + } + ] + } + }, + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + } + }, + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + } + }, + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + }, + "BootstrapVersion": { + "id": "BootstrapVersion", + "path": "sns-grant-subscribe-stack/BootstrapVersion", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + }, + "CheckBootstrapVersion": { + "id": "CheckBootstrapVersion", + "path": "sns-grant-subscribe-stack/CheckBootstrapVersion", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + } + }, + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + }, + "sns-grant-subscribe-test": { + "id": "sns-grant-subscribe-test", + "path": "sns-grant-subscribe-test", + "children": { + "DefaultTest": { + "id": "DefaultTest", + "path": "sns-grant-subscribe-test/DefaultTest", + "children": { + "Default": { + "id": "Default", + "path": "sns-grant-subscribe-test/DefaultTest/Default", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + }, + "DeployAssert": { + "id": "DeployAssert", + "path": "sns-grant-subscribe-test/DefaultTest/DeployAssert", + "children": { + "BootstrapVersion": { + "id": "BootstrapVersion", + "path": "sns-grant-subscribe-test/DefaultTest/DeployAssert/BootstrapVersion", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + }, + "CheckBootstrapVersion": { + "id": "CheckBootstrapVersion", + "path": "sns-grant-subscribe-test/DefaultTest/DeployAssert/CheckBootstrapVersion", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + } + }, + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests-alpha.IntegTestCase", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests-alpha.IntegTest", + "version": "0.0.0" + } + }, + "Tree": { + "id": "Tree", + "path": "Tree", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + } + }, + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns-topic-grant-subscribe.ts b/packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns-topic-grant-subscribe.ts new file mode 100644 index 0000000000000..9e8c2aace105b --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-sns/test/integ.sns-topic-grant-subscribe.ts @@ -0,0 +1,35 @@ +import { Key } from 'aws-cdk-lib/aws-kms'; +import { App, Stack, StackProps, RemovalPolicy, Duration } from 'aws-cdk-lib'; +import { Topic } from 'aws-cdk-lib/aws-sns'; +import { User } from 'aws-cdk-lib/aws-iam'; + +import { IntegTest } from '@aws-cdk/integ-tests-alpha'; + +class SNSInteg extends Stack { + constructor(scope: App, id: string, props?: StackProps) { + super(scope, id, props); + + const key = new Key(this, 'CustomKey', { + pendingWindow: Duration.days(7), + removalPolicy: RemovalPolicy.DESTROY, + }); + + const topic = new Topic(this, 'MyTopic', { + masterKey: key, + }); + + const user = new User(this, 'MyUser'); + + topic.grantSubscribe(user); + } +} + +const app = new App(); + +const stack = new SNSInteg(app, 'sns-grant-subscribe-stack'); + +new IntegTest(app, 'sns-grant-subscribe-test', { + testCases: [stack], +}); + +app.synth(); diff --git a/packages/aws-cdk-lib/aws-sns/README.md b/packages/aws-cdk-lib/aws-sns/README.md index 78de0cb3121dd..b4b219319b536 100644 --- a/packages/aws-cdk-lib/aws-sns/README.md +++ b/packages/aws-cdk-lib/aws-sns/README.md @@ -19,8 +19,8 @@ const topic = new sns.Topic(this, 'Topic', { }); ``` -Add an SNS Topic to your stack with a specified signature version, which corresponds -to the hashing algorithm used while creating the signature of the notifications, +Add an SNS Topic to your stack with a specified signature version, which corresponds +to the hashing algorithm used while creating the signature of the notifications, subscription confirmations, or unsubscribe confirmation messages sent by Amazon SNS. The default signature version is `1` (`SHA1`). @@ -61,6 +61,16 @@ myTopic.addSubscription(new subscriptions.SqsSubscription(queue)); Note that subscriptions of queues in different accounts need to be manually confirmed by reading the initial message from the queue and visiting the link found in it. + The `grantSubscribe` method adds a policy statement to the topic's resource policy, allowing the specified principal to perform the `sns:Subscribe` action. + It's useful when you want to allow entities, such as another AWS account or resources created later, to subscribe to the topic at their own pace, separating permission granting from the actual subscription process. + +```ts +declare const accountPrincipal: iam.AccountPrincipal; +const myTopic = new sns.Topic(this, 'MyTopic'); + +myTopic.grantSubscribe(accountPrincipal); +``` + ### Filter policy A filter policy can be specified when subscribing an endpoint to a topic. 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 7e91e89038a6c..4a50c87c570bb 100644 --- a/packages/aws-cdk-lib/aws-sns/lib/topic-base.ts +++ b/packages/aws-cdk-lib/aws-sns/lib/topic-base.ts @@ -57,6 +57,11 @@ export interface ITopic extends IResource, notifications.INotificationRuleTarget * Grant topic publishing permissions to the given identity */ grantPublish(identity: iam.IGrantable): iam.Grant; + + /** + * Grant topic subscribing permissions to the given identity + */ + grantSubscribe(identity: iam.IGrantable): iam.Grant; } /** @@ -152,7 +157,7 @@ export abstract class TopicBase extends Resource implements ITopic { * For more information, see https://docs.aws.amazon.com/sns/latest/dg/sns-security-best-practices.html#enforce-encryption-data-in-transit. */ protected createSSLPolicyDocument(): iam.PolicyStatement { - return new iam.PolicyStatement ({ + return new iam.PolicyStatement({ sid: 'AllowPublishThroughSSLOnly', actions: ['sns:Publish'], effect: iam.Effect.DENY, @@ -176,6 +181,18 @@ export abstract class TopicBase extends Resource implements ITopic { }); } + /** + * Grant topic subscribing permissions to the given identity + */ + public grantSubscribe(grantee: iam.IGrantable) { + return iam.Grant.addToPrincipalOrResource({ + grantee, + actions: ['sns:Subscribe'], + resourceArns: [this.topicArn], + resource: this, + }); + } + /** * Represents a notification target * That allows SNS topic to associate with this rule target. 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 6c42dd444c441..fc1f1cd4c2846 100644 --- a/packages/aws-cdk-lib/aws-sns/test/sns.test.ts +++ b/packages/aws-cdk-lib/aws-sns/test/sns.test.ts @@ -276,6 +276,31 @@ describe('Topic', () => { }); + test('give subscribing permissions', () => { + // GIVEN + const stack = new cdk.Stack(); + const topic = new sns.Topic(stack, 'Topic'); + const user = new iam.User(stack, 'User'); + + // WHEN + topic.grantSubscribe(user); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { + 'PolicyDocument': { + Version: '2012-10-17', + 'Statement': [ + { + 'Action': 'sns:Subscribe', + 'Effect': 'Allow', + 'Resource': stack.resolve(topic.topicArn), + }, + ], + }, + }); + + }); + test('TopicPolicy passed document', () => { // GIVEN const stack = new cdk.Stack();