Search code examples
aws-cdk

AWS CDK: How to create an IAM role that can be assumed by multiple principals?


I'm deploying a Lambda function that will be used by CloudFront. The execution role for the function therefore needs to be assumed by edgelambda.amazonaws.com and lambda.amazonaws.com. If I was doing this by hand, the policy would look like this:

{
    "Version": "2012-10-17",
    "Statement": [
    {
        "Effect": "Allow",
        "Principal": {
            "Service": [
                "edgelambda.amazonaws.com",
                "lambda.amazonaws.com"
            ]
        },
        "Action": "sts:AssumeRole"
    }
    ]
}

When setting this up in AWS CDK, the iam.Role class only allows you to specify one assuming principal initially, e.g.:

lambda_role = iam.Role(
    self,
    "lambda_redirect_role",
    assumed_by=iam.ServicePrincipal("edgelambda.amazonaws.com"),
    managed_policies=[
        iam.ManagedPolicy.from_aws_managed_policy_name("service-role/AWSLambdaBasicExecutionRole"),
        iam.ManagedPolicy.from_aws_managed_policy_name("AWSXrayWriteOnlyAccess")
    ],
)

So I'm trying to find the best/cleanest way of adding the second principal. The documentation says that I can use assume_role_policy to retrieve the policy document and then manipulate that.

So I've tried:

policy = lambda_role.assume_role_policy
policy.add_statements(
    iam.PolicyStatement(
        actions=["sts:AssumeRole"],
        effect=iam.Effect.ALLOW,
        principals=[
            iam.ServicePrincipal("lambda.amazonaws.com")
        ]
    )
)

but that gives me this rather less-than-optimal policy:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "edgelambda.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    },
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "lambda.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}

I've tried manipulating the existing statement within the policy but I can't figure out how to then get that back into the Role definition:

policy = lambda_role.assume_role_policy
# This gives us a read-only policy so we need to do
# some manipulation in order to add the lambda principal
policy_json = policy.to_json()
print(policy_json)
# That gets us a dict (even though it says json) so now
# we extract the one and only statement ...
statement = policy_json["Statement"][0]
# Turn it back into a CDK object we can manipulate
statement_obj = iam.PolicyStatement.from_json(statement)
# Add the extra principal
statement_obj.add_principals(
    iam.ServicePrincipal("lambda.amazonaws.com")
)
# Put it all back ...
policy_json["Statement"][0] = statement_obj.to_json()
policy.from_json(policy_json)

Is there a way to get to the "cleaner" policy statement with CDK or am I stuck with the two statements it is currently generating?


Solution

  • This can be done using CompositePrincipal:

    lambda_role = iam.Role(
        self,
        "lambda_redirect_role",
        assumed_by=iam.CompositePrincipal(
            iam.ServicePrincipal("edgelambda.amazonaws.com"),
            iam.ServicePrincipal("lambda.amazonaws.com"),
        ),
        ...
    )