AWS CDK - inline IAM Policies with conflicting names are generated for different stacks using a shared role

Viewed 789

I'm using the CDK to deploy several stacks, and one of the roles used is shared across multiple stacks. The constructs (e.g. CodeBuildAction) which use the role frequently attach necessary permissions as an inline policy. However, despite knowing that it is an "imported" role, the inline policy name that is generated is not unique across stacks, and therefore both CloudFormation stacks contain the same Policy resource, and fight over the contents. (Neither stack contains the Role resource.)

import * as cdk from "@aws-cdk/core";
import * as iam from "@aws-cdk/aws-iam";

const sharedRoleArn = "arn:aws:iam::1111111111:role/MyLambdaRole";

const app = new cdk.App();
const stackOne = new cdk.Stack(app, "StackOne");
const roleRefOne = iam.Role.fromRoleArn(stackOne, "SharedRole", sharedRoleArn);

// Under normal circumstances, this is called inside constructs defined by AWS
// (like a CodeBuildAction that grants permission to access Artifact S3 buckets, etc)
roleRefOne.addToPrincipalPolicy(new iam.PolicyStatement({
  actions: ["s3:ListBucket"],
  resources: ["*"],
  effect: iam.Effect.ALLOW,
}));

const stackTwo = new cdk.Stack(app, "StackTwo");
const roleRefTwo = iam.Role.fromRoleArn(stackTwo, "SharedRole", sharedRoleArn);
roleRefTwo.addToPrincipalPolicy(new iam.PolicyStatement({
  actions: ["dynamodb:List*"],
  resources: ["*"],
  effect: iam.Effect.ALLOW,
}));

The following are fragments of the cloud assembly generated for the two stacks:

  SharedRolePolicyA1DDBB1E:
    Type: AWS::IAM::Policy
    Properties:
      PolicyDocument:
        Statement:
          - Action: s3:ListBucket
            Effect: Allow
            Resource: "*"
        Version: "2012-10-17"
      PolicyName: SharedRolePolicyA1DDBB1E
      Roles:
        - MyLambdaRole
    Metadata:
      aws:cdk:path: StackOne/SharedRole/Policy/Resource
  SharedRolePolicyA1DDBB1E:
    Type: AWS::IAM::Policy
    Properties:
      PolicyDocument:
        Statement:
          - Action: dynamodb:List*
            Effect: Allow
            Resource: "*"
        Version: "2012-10-17"
      PolicyName: SharedRolePolicyA1DDBB1E
      Roles:
        - MyLambdaRole
    Metadata:
      aws:cdk:path: StackTwo/SharedRole/Policy/Resource

You can see above that the aws:cdk:paths for two policies are different, but they end up with the same name (SharedRolePolicyA1DDBB1E). That is used as the physical name of the inline policy attached to the MySharedRole role. (The same behavior occurs for stacks in separate "Apps" as well.)

There's no affordance for setting the PolicyName for the "default policy" generated for a role (or which policies a construct attaches permissions to). I could also make the shared role immutable (using { mutable: false } on fromRoleArn, but then I need to reconstruct the potentially complicated Policies a set of constructs would have given the role, and attache it myself.

I was able to work around the issue by templating the stack name into the imported role's "id", as in:

const stack = cdk.Stack.of(scope)
const role = iam.Role.fromRoleArn(scope, `${stack.stackName}SharedRole`, sharedRoleArn);

where I construct my role.

Is this expected behavior? Do I misunderstand something about imported resources with CDK? Is there a better alternative? (My understanding with the construct ids is that they are only intended to need to be unique within a given scope.)

0 Answers
Related