AWS CDK testing references

Viewed 1107

I am trying to unit-test my CDK application. I have a role created and I want to assure that it has all the policies assigned. As roles and policies are different resources, policies are not available from Cloud Formation Role resource. Role only has a reference to the policy:

"MyRole4CBCE4C9": {
      "Type": "AWS::IAM::Role",
      "Properties": {
        "AssumeRolePolicyDocument": {
          "Statement": [
            {
              ...
            }
          ],
          "Version": "2012-10-17"
        },
        "ManagedPolicyArns": [
          {
            "Ref": "MyPolicyC18AB378"
          }
        ]
      },

In the test I have:

expectCDK(stack).to(haveResource("AWS::IAM::Role", {
                AssumeRolePolicyDocument: {
                    Statement: [
                        ...
                    ],
                    Version: "2012-10-17",
                },
            }
        ));

How can I validate that this exact role has correct policy? Steps I have in my head are as follows:

  1. Get "Ref" from the Role properties
  2. Find Policy by this reference
  3. Assert all the necessary data in Policy

However, it seems that CDK does not provide functions to get the element by its logical id and to get resource from haveResource as an object.

What is CDK way to approach this kind of testing?


UPD: seems like I can approach it with StackInspector, though I still wonder, what is the true way for this.

1 Answers

Scenario 1: you're creating a role and a policy in the same stack, and the policy will not be reused in any other stack.

In this case use iam.Policy and attach it in-line:

export class CdkGetLogicalIdExampleStack extends cdk.Stack {
  role: iam.IRole;

  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const policy = new iam.Policy(this, 'policy', {
      policyName: 'policy',
      statements: [
        new iam.PolicyStatement({
          actions: ["s3:*"],
          resources: ['*'],
          effect: iam.Effect.ALLOW
        })
      ]
    });

    this.role = new iam.Role(this, 'my-role', {
      roleName: 'my-role',
      assumedBy: new iam.AccountPrincipal(this.account),
    });

    // Alternatively: this.role.attachInlinePolicy(policy);
    policy.attachToRole(this.role);
  }
}

The role is kept as stack's attribute, so it's easy to refer to it in tests:

test('Empty Stack', () => {
  const app = new cdk.App();
  const stack = new CdkGetLogicalIdExample.CdkGetLogicalIdExampleStack(app, 'MyTestStack');
  const roleId = stack.getLogicalId(stack.role.node.findChild('Resource') as cdk.CfnElement);

  expectCDK(stack).to(haveResource("AWS::IAM::Policy", {
    "PolicyDocument": {
      "Statement": [
        {
          "Action": "s3:*",
          "Effect": "Allow",
          "Resource": "*"
        }
      ],
      "Version": "2012-10-17"
    },
    "PolicyName": "policy",
    "Roles": [
      {
        "Ref": roleId
      }
    ]
  }));
});

Scenario 2: you're creating a policy that can be used in various stacks, so you want to make it a managed policy.

Testing this scenario is a little bit more complex because iam.ManagedPolicy implements the IManagedPolicy interface, which only provides the managedPolicyArn attribute (I am using CDK 1.124.0).

Nevertheless, the iam.ManagedPolicy extends the cdk.Resource, so we can trick the casting mechanism of TypeScript in the following way:

export class CdkGetLogicalIdExampleStack extends cdk.Stack {
  // note the type here
  managedPolicy: cdk.Resource | iam.IManagedPolicy;
  role: iam.IRole;

  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    this.managedPolicy = new iam.ManagedPolicy(this, 'managed-policy', {
      managedPolicyName: 'managed-policy',
      document: new iam.PolicyDocument({
        statements: [
          new iam.PolicyStatement({
            actions: ["dynamodb:*"],
            resources: ["*"],
            effect: iam.Effect.ALLOW
          })
        ]
      })
    });

    this.role = new iam.Role(this, 'my-role', {
      roleName: 'my-role',
      assumedBy: new iam.AccountPrincipal(this.account),
    });

    // here we need to cast to iam.IManagedPolicy
    this.role.addManagedPolicy(this.managedPolicy as iam.IManagedPolicy);
  }
}

Now, testing it is possible because we can access managedPolicy attribute as cdk.Resource:

  const roleId = stack.getLogicalId(stack.role.node.findChild('Resource') as cdk.CfnElement);
  // here we do the casting to cdk.Resource
  const managedPolicyResource = stack.managedPolicy as cdk.Resource;
  const managedPolicyId = stack.getLogicalId(managedPolicyResource.node.findChild('Resource') as cdk.CfnElement);

  expectCDK(stack).to(haveResource("AWS::IAM::ManagedPolicy", {
    "PolicyDocument": {
      "Statement": [
        {
          "Action": "dynamodb:*",
          "Effect": "Allow",
          "Resource": "*"
        }
      ],
      "Version": "2012-10-17"
    },
    "Description": "",
    "ManagedPolicyName": "managed-policy",
    "Path": "/"
  }));

  expectCDK(stack).to(haveResource("AWS::IAM::Role", {
    "AssumeRolePolicyDocument": {
      "Statement": [
        {
          "Action": "sts:AssumeRole",
          "Effect": "Allow",
          "Principal": {
            "AWS": {
              "Fn::Join": [
                "",
                [
                  "arn:",
                  {
                    "Ref": "AWS::Partition"
                  },
                  ":iam::",
                  {
                    "Ref": "AWS::AccountId"
                  },
                  ":root"
                ]
              ]
            }
          }
        }
      ],
      "Version": "2012-10-17"
    },
    "ManagedPolicyArns": [
      {
        "Ref": managedPolicyId
      }
    ],
    "RoleName": "my-role"
  }));

In this way you can test that your policies have all the necessary data and the relations between roles and policies are correct.

You can access a full working example here.

Related