(aws-iam): `Role.grantX()` doesn't create implicit dependency for attached policy resources
- Dominant language
- TypeScript
- Stars
- 12.9k
- Forks
- 4.6k
- Avg merge
- 2d 3h
- Merged PRs (30d)
- 83
Description
### Describe the bug
Using `stream.grantWrite(role)` to grant permissions does not seem to establish a CloudFormation dependency for the generated IAM Policy, causing resources like `CfnDestination` that use the `role.roleArn` to fail deployment due to missing permissions during creation/validation.
### Regression Issue
- [ ] Select this option if this issue appears to be a regression.
### Last Known Working CDK Version
_No response_
### Reproduction Steps
Consider the following CDK TypeScript code which sets up a CloudWatch Logs Destination (`CfnDestination`) pointing to a Kinesis Stream (`Stream`). A role (`Role`) is created for CloudWatch Logs to assume, and permissions to write to the Kinesis stream are granted using `stream.grantWrite(role)`:
```typescript
import { RemovalPolicy, Stack, StackProps } from "aws-cdk-lib";
import { Role, ServicePrincipal } from "aws-cdk-lib/aws-iam";
import { Stream } from "aws-cdk-lib/aws-kinesis";
import { CfnDestination } from "aws-cdk-lib/aws-logs";
import { Construct } from "constructs";
export class MonitoringStack extends Stack {
constructor(scope: Construct, id: string, props: StackProps = {}) {
super(scope, id, props);
const DESTINATION_NAME = "test-destination";
// Create a Kinesis stream
const testStream = new Stream(this, "TestStream", {
shardCount: 1,
removalPolicy: RemovalPolicy.DESTROY,
});
// Role for CloudWatch Logs to assume
const cwlToKinesisRole = new Role(this, "CwlToKinesisRole", {
assumedBy: new ServicePrincipal("logs.amazonaws.com"),
});
// Grant write permissions to the role
testStream.grantWrite(cwlToKinesisRole);
// Create the CloudWatch Logs Destination
new CfnDestination(this, "TestDestination", {
destinationName: DESTINATION_NAME,
targetArn: testStream.streamArn,
roleArn: cwlToKinesisRole.roleArn,
destinationPolicy: JSON.stringify({
Version: "2012-10-17",
Statement: [
{
Sid: "AllowSubscriptionFilter",
Effect: "Allow",
Principal: "*",
Action: ["logs:PutSubscriptionFilter", "logs:PutAccountPolicy"],
Resource: `arn:aws:logs:${this.region}:${this.account}:destination:${DESTINATION_NAME}`,
Condition: {
StringEquals: {
"aws:PrincipalOrgID": ["o-xxxxxxxxxx"],
},
},
},
],
}),
});
}
}
```
**Observed Behavior**
When deploying this stack, the creation of the `AWS::Logs::Destination` resource (`TestDestination`) fails with the following CloudFormation error:
```
CREATE_FAILED | AWS::Logs::Destination | TestDestination
Resource handler returned message: "Invalid request provided: AWS::Logs::Destination. Could not deliver test message to specified destination. Check if the destination is valid. (Service: CloudWatchLogs, Status Code: 400, ...)"
```
It appears there's a race condition. The `CfnDestination` resource attempts creation before the IAM Policy granting `kinesis:PutRecord` permissions (generated by `testStream.grantWrite()`) is fully attached to the `cwlToKinesisRole`. Since the role lacks the necessary permissions at the moment CloudWatch Logs tries to validate the destination by sending a test message, the creation fails.
**Expected Behavior**
Calling `testStream.grantWrite(role)` should ideally create an implicit dependency. The `CfnDestination` resource, which uses `role.roleArn`, should depend not only on the `AWS::IAM::Role` resource itself but also on the `AWS::IAM::Policy` resource generated by the `grantWrite()` call that attaches permissions *to* that role. This would ensure the permissions are in place before the destination is created and validated.
### Possible Solution
Defining the policy inline within the `Role` construct resolves the issue, because it forces CloudFormation to create the role and its inline policy together, before the `CfnDestination` (which depends from the `roleArn`) is created:
```typescript
import { Effect, PolicyDocument, PolicyStatement, Role, ServicePrincipal } from "aws-cdk-lib/aws-iam";
import { Stream } from "aws-cdk-lib/aws-kinesis";
import { CfnDestination } from "aws-cdk-lib/aws-logs";
import { Construct } from "constructs";
import { RemovalPolicy, Stack, StackProps } from "aws-cdk-lib";
export class MonitoringStack extends Stack {
constructor(scope: Construct, id: string, props: StackProps = {}) {
super(scope, id, props);
const DESTINATION_NAME = "test-destination";
// Create a Kinesis stream
const testStream = new Stream(this, "TestStream", {
shardCount: 1,
removalPolicy: RemovalPolicy.DESTROY,
});
// Role for CloudWatch Logs to assume
const cwlToKinesisRole = new Role(this, "CwlToKinesisRole", {
assumedBy: new ServicePrincipal("logs.amazonaws.com"),
inlinePolicies: {
KinesisWritePolicy: new PolicyDocument({
statements: [
new PolicyStatement({
actions: ["kinesis:PutRecord", "kinesis:PutRecords"],
resources: [testStream.streamArn],
effect: Effect.ALLOW,
}),
],
}),
},
});
// Create the CloudWatch Logs Destination
new CfnDestination(this, "TestDestination", {
destinationName: DESTINATION_NAME,
targetArn: testStream.streamArn,
roleArn: cwlToKinesisRole.roleArn,
destinationPolicy: JSON.stringify({
Version: "2012-10-17",
Statement: [
{
Sid: "AllowSubscriptionFilter",
Effect: "Allow",
Principal: "*",
Action: ["logs:PutSubscriptionFilter", "logs:PutAccountPolicy"],
Resource: `arn:aws:logs:${this.region}:${this.account}:destination:${DESTINATION_NAME}`,
Condition: {
StringEquals: {
"aws:PrincipalOrgID": ["o-xxxxxxxxxx"],
},
},
},
],
}),
});
}
}
```
### Additional Information/Context
_No response_
### CDK CLI Version
2.192.0
### Framework Version
_No response_
### Node.js Version
22.15.0
### OS
Ubuntu 24.04
### Language
TypeScript
### Language Version
_No response_
### Other information
_No response_
Contributor guide
Research direction
Start with the TypeScript reproduction using Stream.grantWrite(), Role.roleArn, and CfnDestination, then synthesize the stack and inspect the generated AWS::IAM::Policy and resource dependencies. Done means the destination waits for the grant-generated policy attachment, with a regression test covering the synthesized dependency and deployment ordering.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- aws, typescript
- Domain
- infrastructure, security
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Stale
- Clarity
- Mostly clear
- Newbie friendliness
- 42/100