Skip to content

Commit 74318c7

Browse files
authored
fix(events-targets): EventBus IAM statements are only added for the first target (#20479)
If the `EventBus` constructor is called with no arguments, then attaching more than a single target to its policy will silently fail to add them. This is because of a strange edge case in the implementation that was not accounted for previously; it is possible for `props.role` to be `undefined`, yet `singletonEventRole()` is still capable of finding the desired role. `singletonEventRole()` does not add the new statements to any IAM policies that it finds, so as a result adding multiple targets does not add any of them. Fixes #19407. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent 264c02e commit 74318c7

12 files changed

+138
-55
lines changed

packages/@aws-cdk/aws-events-targets/lib/api-destination.ts

+7-4
Original file line numberDiff line numberDiff line change
@@ -83,13 +83,16 @@ export class ApiDestination implements events.IRuleTarget {
8383
addToDeadLetterQueueResourcePolicy(_rule, this.props.deadLetterQueue);
8484
}
8585

86+
const role = this.props?.eventRole ?? singletonEventRole(this.apiDestination);
87+
role.addToPrincipalPolicy(new iam.PolicyStatement({
88+
resources: [this.apiDestination.apiDestinationArn],
89+
actions: ['events:InvokeApiDestination'],
90+
}));
91+
8692
return {
8793
...(this.props ? bindBaseTargetConfig(this.props) : {}),
8894
arn: this.apiDestination.apiDestinationArn,
89-
role: this.props?.eventRole ?? singletonEventRole(this.apiDestination, [new iam.PolicyStatement({
90-
resources: [this.apiDestination.apiDestinationArn],
91-
actions: ['events:InvokeApiDestination'],
92-
})]),
95+
role,
9396
input: this.props.event,
9497
targetResource: this.apiDestination,
9598
httpParameters,

packages/@aws-cdk/aws-events-targets/lib/api-gateway.ts

+11-7
Original file line numberDiff line numberDiff line change
@@ -98,16 +98,20 @@ export class ApiGateway implements events.IRuleTarget {
9898
this.props?.path || '/',
9999
this.props?.stage || this.restApi.deploymentStage.stageName,
100100
);
101+
102+
const role = this.props?.eventRole || singletonEventRole(this.restApi);
103+
role.addToPrincipalPolicy(new iam.PolicyStatement({
104+
resources: [restApiArn],
105+
actions: [
106+
'execute-api:Invoke',
107+
'execute-api:ManageConnections',
108+
],
109+
}));
110+
101111
return {
102112
...(this.props ? bindBaseTargetConfig(this.props) : {}),
103113
arn: restApiArn,
104-
role: this.props?.eventRole || singletonEventRole(this.restApi, [new iam.PolicyStatement({
105-
resources: [restApiArn],
106-
actions: [
107-
'execute-api:Invoke',
108-
'execute-api:ManageConnections',
109-
],
110-
})]),
114+
role,
111115
deadLetterConfig: this.props?.deadLetterQueue && { arn: this.props.deadLetterQueue?.queueArn },
112116
input: this.props?.postBody,
113117
targetResource: this.restApi,

packages/@aws-cdk/aws-events-targets/lib/batch.ts

+12-11
Original file line numberDiff line numberDiff line change
@@ -87,20 +87,21 @@ export class BatchJob implements events.IRuleTarget {
8787
addToDeadLetterQueueResourcePolicy(rule, this.props.deadLetterQueue);
8888
}
8989

90+
// When scoping resource-level access for job submission, you must provide both job queue and job definition resource types.
91+
// https://docs.aws.amazon.com/batch/latest/userguide/ExamplePolicies_BATCH.html#iam-example-restrict-job-def
92+
const role = singletonEventRole(this.jobDefinitionScope);
93+
role.addToPrincipalPolicy(new iam.PolicyStatement({
94+
actions: ['batch:SubmitJob'],
95+
resources: [
96+
this.jobDefinitionArn,
97+
this.jobQueueArn,
98+
],
99+
}));
100+
90101
return {
91102
...bindBaseTargetConfig(this.props),
92103
arn: this.jobQueueArn,
93-
// When scoping resource-level access for job submission, you must provide both job queue and job definition resource types.
94-
// https://docs.aws.amazon.com/batch/latest/userguide/ExamplePolicies_BATCH.html#iam-example-restrict-job-def
95-
role: singletonEventRole(this.jobDefinitionScope, [
96-
new iam.PolicyStatement({
97-
actions: ['batch:SubmitJob'],
98-
resources: [
99-
this.jobDefinitionArn,
100-
this.jobQueueArn,
101-
],
102-
}),
103-
]),
104+
role,
104105
input: this.props.event,
105106
targetResource: this.jobQueueScope,
106107
batchParameters,

packages/@aws-cdk/aws-events-targets/lib/codebuild.ts

+7-6
Original file line numberDiff line numberDiff line change
@@ -44,15 +44,16 @@ export class CodeBuildProject implements events.IRuleTarget {
4444
addToDeadLetterQueueResourcePolicy(_rule, this.props.deadLetterQueue);
4545
}
4646

47+
const role = this.props.eventRole || singletonEventRole(this.project);
48+
role.addToPrincipalPolicy(new iam.PolicyStatement({
49+
actions: ['codebuild:StartBuild'],
50+
resources: [this.project.projectArn],
51+
}));
52+
4753
return {
4854
...bindBaseTargetConfig(this.props),
4955
arn: this.project.projectArn,
50-
role: this.props.eventRole || singletonEventRole(this.project, [
51-
new iam.PolicyStatement({
52-
actions: ['codebuild:StartBuild'],
53-
resources: [this.project.projectArn],
54-
}),
55-
]),
56+
role,
5657
input: this.props.event,
5758
targetResource: this.project,
5859
};

packages/@aws-cdk/aws-events-targets/lib/codepipeline.ts

+7-4
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,17 @@ export class CodePipeline implements events.IRuleTarget {
2626
}
2727

2828
public bind(_rule: events.IRule, _id?: string): events.RuleTargetConfig {
29+
const role = this.options.eventRole || singletonEventRole(this.pipeline);
30+
role.addToPrincipalPolicy(new iam.PolicyStatement({
31+
resources: [this.pipeline.pipelineArn],
32+
actions: ['codepipeline:StartPipelineExecution'],
33+
}));
34+
2935
return {
3036
...bindBaseTargetConfig(this.options),
3137
id: '',
3238
arn: this.pipeline.pipelineArn,
33-
role: this.options.eventRole || singletonEventRole(this.pipeline, [new iam.PolicyStatement({
34-
resources: [this.pipeline.pipelineArn],
35-
actions: ['codepipeline:StartPipelineExecution'],
36-
})]),
39+
role,
3740
targetResource: this.pipeline,
3841
};
3942
}

packages/@aws-cdk/aws-events-targets/lib/ecs-task.ts

+3-6
Original file line numberDiff line numberDiff line change
@@ -118,12 +118,9 @@ export class EcsTask implements events.IRuleTarget {
118118
this.taskCount = props.taskCount ?? 1;
119119
this.platformVersion = props.platformVersion;
120120

121-
if (props.role) {
122-
const role = props.role;
123-
this.createEventRolePolicyStatements().forEach(role.addToPrincipalPolicy.bind(role));
124-
this.role = role;
125-
} else {
126-
this.role = singletonEventRole(this.taskDefinition, this.createEventRolePolicyStatements());
121+
this.role = props.role ?? singletonEventRole(this.taskDefinition);
122+
for (const stmt of this.createEventRolePolicyStatements()) {
123+
this.role.addToPrincipalPolicy(stmt);
127124
}
128125

129126
// Security groups are only configurable with the "awsvpc" network mode.

packages/@aws-cdk/aws-events-targets/lib/event-bus.ts

+2-4
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,8 @@ export class EventBus implements events.IRuleTarget {
3636
constructor(private readonly eventBus: events.IEventBus, private readonly props: EventBusProps = {}) { }
3737

3838
bind(rule: events.IRule, _id?: string): events.RuleTargetConfig {
39-
if (this.props.role) {
40-
this.props.role.addToPrincipalPolicy(this.putEventStatement());
41-
}
42-
const role = this.props.role ?? singletonEventRole(rule, [this.putEventStatement()]);
39+
const role = this.props.role ?? singletonEventRole(rule);
40+
role.addToPrincipalPolicy(this.putEventStatement());
4341

4442
if (this.props.deadLetterQueue) {
4543
addToDeadLetterQueueResourcePolicy(rule, this.props.deadLetterQueue);

packages/@aws-cdk/aws-events-targets/lib/kinesis-firehose-stream.ts

+5-3
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,16 @@ export class KinesisFirehoseStream implements events.IRuleTarget {
3131
* result from a Event Bridge event.
3232
*/
3333
public bind(_rule: events.IRule, _id?: string): events.RuleTargetConfig {
34-
const policyStatements = [new iam.PolicyStatement({
34+
const role = singletonEventRole(this.stream);
35+
role.addToPrincipalPolicy(new iam.PolicyStatement({
3536
actions: ['firehose:PutRecord', 'firehose:PutRecordBatch'],
3637
resources: [this.stream.attrArn],
37-
})];
38+
}));
39+
3840

3941
return {
4042
arn: this.stream.attrArn,
41-
role: singletonEventRole(this.stream, policyStatements),
43+
role,
4244
input: this.props.message,
4345
targetResource: this.stream,
4446
};

packages/@aws-cdk/aws-events-targets/lib/kinesis-stream.ts

+4-3
Original file line numberDiff line numberDiff line change
@@ -45,14 +45,15 @@ export class KinesisStream implements events.IRuleTarget {
4545
* result from a CloudWatch event.
4646
*/
4747
public bind(_rule: events.IRule, _id?: string): events.RuleTargetConfig {
48-
const policyStatements = [new iam.PolicyStatement({
48+
const role = singletonEventRole(this.stream);
49+
role.addToPrincipalPolicy(new iam.PolicyStatement({
4950
actions: ['kinesis:PutRecord', 'kinesis:PutRecords'],
5051
resources: [this.stream.streamArn],
51-
})];
52+
}));
5253

5354
return {
5455
arn: this.stream.streamArn,
55-
role: singletonEventRole(this.stream, policyStatements),
56+
role,
5657
input: this.props.message,
5758
targetResource: this.stream,
5859
kinesisParameters: this.props.partitionKeyPath ? { partitionKeyPath: this.props.partitionKeyPath } : undefined,

packages/@aws-cdk/aws-events-targets/lib/state-machine.ts

+1-4
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,8 @@ export class SfnStateMachine implements events.IRuleTarget {
2929
private readonly role: iam.IRole;
3030

3131
constructor(public readonly machine: sfn.IStateMachine, private readonly props: SfnStateMachineProps = {}) {
32-
if (props.role) {
33-
props.role.grant(new iam.ServicePrincipal('events.amazonaws.com'));
34-
}
3532
// no statements are passed because we are configuring permissions by using grant* helper below
36-
this.role = props.role ?? singletonEventRole(machine, []);
33+
this.role = props.role ?? singletonEventRole(machine);
3734
machine.grantStartExecution(this.role);
3835
}
3936

packages/@aws-cdk/aws-events-targets/lib/util.ts

+1-3
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ export function bindBaseTargetConfig(props: TargetBaseProps) {
7171
* events have the same target, they will share a role.
7272
* @internal
7373
*/
74-
export function singletonEventRole(scope: IConstruct, policyStatements: iam.PolicyStatement[]): iam.IRole {
74+
export function singletonEventRole(scope: IConstruct): iam.IRole {
7575
const id = 'EventsRole';
7676
const existing = scope.node.tryFindChild(id) as iam.IRole;
7777
if (existing) { return existing; }
@@ -81,8 +81,6 @@ export function singletonEventRole(scope: IConstruct, policyStatements: iam.Poli
8181
assumedBy: new iam.ServicePrincipal('events.amazonaws.com'),
8282
});
8383

84-
policyStatements.forEach(role.addToPolicy.bind(role));
85-
8684
return role;
8785
}
8886

packages/@aws-cdk/aws-events-targets/test/event-bus/event-rule-target.test.ts

+78
Original file line numberDiff line numberDiff line change
@@ -167,3 +167,81 @@ test('with a Dead Letter Queue specified', () => {
167167
],
168168
});
169169
});
170+
171+
test('event buses are correctly added to the rule\'s principal policy', () => {
172+
const stack = new Stack();
173+
const rule = new events.Rule(stack, 'Rule', {
174+
schedule: events.Schedule.expression('rate(1 min)'),
175+
});
176+
177+
const bus1 = new events.EventBus(stack, 'bus' + 1);
178+
const bus2 = new events.EventBus(stack, 'bus' + 2);
179+
180+
rule.addTarget(new targets.EventBus(bus1));
181+
rule.addTarget(new targets.EventBus(bus2));
182+
183+
Template.fromStack(stack).hasResourceProperties('AWS::Events::Rule', {
184+
Targets: [
185+
{
186+
Arn: {
187+
'Fn::GetAtt': [
188+
'bus110C385DC',
189+
'Arn',
190+
],
191+
},
192+
Id: 'Target0',
193+
RoleArn: {
194+
'Fn::GetAtt': [
195+
'RuleEventsRoleC51A4248',
196+
'Arn',
197+
],
198+
},
199+
},
200+
{
201+
Arn: {
202+
'Fn::GetAtt': [
203+
'bus22D01F126',
204+
'Arn',
205+
],
206+
},
207+
Id: 'Target1',
208+
RoleArn: {
209+
'Fn::GetAtt': [
210+
'RuleEventsRoleC51A4248',
211+
'Arn',
212+
],
213+
},
214+
},
215+
],
216+
});
217+
Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', {
218+
PolicyDocument: {
219+
Statement: [
220+
{
221+
Effect: 'Allow',
222+
Action: 'events:PutEvents',
223+
Resource: {
224+
'Fn::GetAtt': [
225+
'bus110C385DC',
226+
'Arn',
227+
],
228+
},
229+
},
230+
{
231+
Effect: 'Allow',
232+
Action: 'events:PutEvents',
233+
Resource: {
234+
'Fn::GetAtt': [
235+
'bus22D01F126',
236+
'Arn',
237+
],
238+
},
239+
},
240+
],
241+
Version: '2012-10-17',
242+
},
243+
Roles: [{
244+
Ref: 'RuleEventsRoleC51A4248',
245+
}],
246+
});
247+
});

0 commit comments

Comments
 (0)