diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-events-targets/test/sns/integ.sns-event-rule-target.js.snapshot/aws-cdk-sns-event-target.template.json b/packages/@aws-cdk-testing/framework-integ/test/aws-events-targets/test/sns/integ.sns-event-rule-target.js.snapshot/aws-cdk-sns-event-target.template.json index cd1aa126d0ea2..346528d1c7154 100644 --- a/packages/@aws-cdk-testing/framework-integ/test/aws-events-targets/test/sns/integ.sns-event-rule-target.js.snapshot/aws-cdk-sns-event-target.template.json +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-events-targets/test/sns/integ.sns-event-rule-target.js.snapshot/aws-cdk-sns-event-target.template.json @@ -3,28 +3,42 @@ "MyTopic86869434": { "Type": "AWS::SNS::Topic" }, - "MyTopicPolicy12A5EC17": { - "Type": "AWS::SNS::TopicPolicy", + "MyTopicEventsRole0D2D3332": { + "Type": "AWS::IAM::Role", "Properties": { - "PolicyDocument": { + "AssumeRolePolicyDocument": { "Statement": [ { - "Action": "sns:Publish", + "Action": "sts:AssumeRole", "Effect": "Allow", "Principal": { "Service": "events.amazonaws.com" - }, + } + } + ], + "Version": "2012-10-17" + } + } + }, + "MyTopicEventsRoleDefaultPolicyB79AC002": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "sns:Publish", + "Effect": "Allow", "Resource": { "Ref": "MyTopic86869434" - }, - "Sid": "0" + } } ], "Version": "2012-10-17" }, - "Topics": [ + "PolicyName": "MyTopicEventsRoleDefaultPolicyB79AC002", + "Roles": [ { - "Ref": "MyTopic86869434" + "Ref": "MyTopicEventsRole0D2D3332" } ] } @@ -47,7 +61,13 @@ ] } }, - "Id": "Target0" + "Id": "Target0", + "RoleArn": { + "Fn::GetAtt": [ + "MyTopicEventsRole0D2D3332", + "Arn" + ] + } } ] } @@ -94,19 +114,21 @@ }, "MyQueueawscdksnseventtargetMyTopicB7575CD87304D383": { "Type": "AWS::SNS::Subscription", - "DependsOn": "MyQueuePolicy6BBEDDAC", "Properties": { - "Protocol": "sqs", - "TopicArn": { - "Ref": "MyTopic86869434" - }, "Endpoint": { "Fn::GetAtt": [ "MyQueueE6CA6235", "Arn" ] + }, + "Protocol": "sqs", + "TopicArn": { + "Ref": "MyTopic86869434" } - } + }, + "DependsOn": [ + "MyQueuePolicy6BBEDDAC" + ] }, "MyDeadLetterQueueD997968A": { "Type": "AWS::SQS::Queue", @@ -187,4 +209,4 @@ ] } } -} \ No newline at end of file +} diff --git a/packages/aws-cdk-lib/aws-events-targets/lib/sns.ts b/packages/aws-cdk-lib/aws-events-targets/lib/sns.ts index 75fd345174f02..b7aae3497f8b3 100644 --- a/packages/aws-cdk-lib/aws-events-targets/lib/sns.ts +++ b/packages/aws-cdk-lib/aws-events-targets/lib/sns.ts @@ -1,4 +1,4 @@ -import { addToDeadLetterQueueResourcePolicy, TargetBaseProps, bindBaseTargetConfig } from './util'; +import { addToDeadLetterQueueResourcePolicy, TargetBaseProps, bindBaseTargetConfig, singletonEventRole } from './util'; import * as events from '../../aws-events'; import * as iam from '../../aws-iam'; import * as sns from '../../aws-sns'; @@ -13,6 +13,13 @@ export interface SnsTopicProps extends TargetBaseProps { * @default the entire EventBridge event */ readonly message?: events.RuleTargetInput; + + /** + * Role to be used to publish the event + * + * @default a new role is created. + */ + readonly role?: iam.IRole; } /** @@ -37,8 +44,12 @@ export class SnsTopic implements events.IRuleTarget { * @see https://docs.aws.amazon.com/eventbridge/latest/userguide/resource-based-policies-eventbridge.html#sns-permissions */ public bind(_rule: events.IRule, _id?: string): events.RuleTargetConfig { - // deduplicated automatically - this.topic.grantPublish(new iam.ServicePrincipal('events.amazonaws.com')); + const role = this.props.role || singletonEventRole(this.topic); + + role.addToPrincipalPolicy(new iam.PolicyStatement({ + actions: ['sns:Publish'], + resources: [this.topic.topicArn], + })); if (this.props.deadLetterQueue) { addToDeadLetterQueueResourcePolicy(_rule, this.props.deadLetterQueue); @@ -49,6 +60,7 @@ export class SnsTopic implements events.IRuleTarget { arn: this.topic.topicArn, input: this.props.message, targetResource: this.topic, + role, }; } } diff --git a/packages/aws-cdk-lib/aws-events-targets/test/sns/sns.test.ts b/packages/aws-cdk-lib/aws-events-targets/test/sns/sns.test.ts index 42569203874cc..e6c3d1d43dfb2 100644 --- a/packages/aws-cdk-lib/aws-events-targets/test/sns/sns.test.ts +++ b/packages/aws-cdk-lib/aws-events-targets/test/sns/sns.test.ts @@ -1,5 +1,6 @@ import { Template } from '../../../assertions'; import * as events from '../../../aws-events'; +import * as iam from '../../../aws-iam'; import * as sns from '../../../aws-sns'; import * as sqs from '../../../aws-sqs'; import { Duration, Stack } from '../../../core'; @@ -17,62 +18,93 @@ test('sns topic as an event rule target', () => { rule.addTarget(new targets.SnsTopic(topic)); // THEN - Template.fromStack(stack).hasResourceProperties('AWS::SNS::TopicPolicy', { + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { + ScheduleExpression: 'rate(1 hour)', + State: 'ENABLED', + Targets: [ + { + Arn: { Ref: 'MyTopic86869434' }, + Id: 'Target0', + RoleArn: { 'Fn::GetAtt': ['MyTopicEventsRole0D2D3332', 'Arn'] }, + }, + ], + }); + + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { Service: 'events.amazonaws.com' }, + }, + ], + Version: '2012-10-17', + }, + }); + + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { - Sid: '0', Action: 'sns:Publish', Effect: 'Allow', - Principal: { Service: 'events.amazonaws.com' }, Resource: { Ref: 'MyTopic86869434' }, }, ], Version: '2012-10-17', }, - Topics: [{ Ref: 'MyTopic86869434' }], }); +}); +test('multiple uses of a topic as a target results in a single policy statement', () => { + // GIVEN + const stack = new Stack(); + const topic = new sns.Topic(stack, 'MyTopic'); + + // WHEN + for (let i = 0; i < 5; ++i) { + const rule = new events.Rule(stack, `Rule${i}`, { + schedule: events.Schedule.rate(Duration.hours(1)), + }); + rule.addTarget(new targets.SnsTopic(topic)); + } + + // THEN Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { - ScheduleExpression: 'rate(1 hour)', - State: 'ENABLED', Targets: [ { Arn: { Ref: 'MyTopic86869434' }, Id: 'Target0', + RoleArn: { 'Fn::GetAtt': ['MyTopicEventsRole0D2D3332', 'Arn'] }, }, ], }); }); -test('multiple uses of a topic as a target results in a single policy statement', () => { +test('specifying custom role for sns project target', () => { // GIVEN const stack = new Stack(); const topic = new sns.Topic(stack, 'MyTopic'); + const rule = new events.Rule(stack, 'MyRule', { + schedule: events.Schedule.rate(Duration.hours(1)), + }); + const role = new iam.Role(stack, 'MyRole', { + assumedBy: new iam.ServicePrincipal('events.amazonaws.com'), + }); // WHEN - for (let i = 0; i < 5; ++i) { - const rule = new events.Rule(stack, `Rule${i}`, { - schedule: events.Schedule.rate(Duration.hours(1)), - }); - rule.addTarget(new targets.SnsTopic(topic)); - } + rule.addTarget(new targets.SnsTopic(topic, { role })); // THEN - Template.fromStack(stack).hasResourceProperties('AWS::SNS::TopicPolicy', { - PolicyDocument: { - Statement: [ - { - Action: 'sns:Publish', - Effect: 'Allow', - Principal: { Service: 'events.amazonaws.com' }, - Resource: { Ref: 'MyTopic86869434' }, - Sid: '0', - }, - ], - Version: '2012-10-17', - }, - Topics: [{ Ref: 'MyTopic86869434' }], + Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', { + Targets: [ + { + Arn: { Ref: 'MyTopic86869434' }, + Id: 'Target0', + RoleArn: { 'Fn::GetAtt': ['MyRoleF48FFE04', 'Arn'] }, + }, + ], }); }); @@ -108,3 +140,4 @@ test('dead letter queue is configured correctly', () => { ], }); }); +