Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(aws-iam): Minimizing assume policies with conditions results in error #28713

Open
Sordie opened this issue Jan 15, 2024 · 10 comments · May be fixed by codu-code/codu#987, gitafolabi/kreuzlaker#2 or NOUIY/aws-solutions-constructs#113
Labels
@aws-cdk/aws-iam Related to AWS Identity and Access Management bug This issue is a bug. effort/medium Medium work item – several days of effort p2

Comments

@Sordie
Copy link

Sordie commented Jan 15, 2024

Describe the bug

When using the AWS CDK with the "@aws-cdk/aws-iam:minimizePolicies": true setting, an error is encountered when creating a Role with a PrincipalWithConditions. The error suggests that the principals in a PolicyStatement must have the same conditions, even though they do have the same conditions.

Expected Behavior

The AWS CDK should successfully create a Role with a PrincipalWithConditions, even when the "@aws-cdk/aws-iam:minimizePolicies" setting is set to true. The conditions specified for the principals in the PolicyStatement should be correctly merged without causing an error.

Current Behavior

The following error is throw:

Error: Resolution error: Resolution error: Resolution error: All principals in a PolicyStatement must have the same Conditions (got '{}' and '{"StringEquals":{"aws:SourceAccount":"${Token[AWS.AccountId.3]}"}}'). Use multiple statements instead..
Object creation stack:
  at stack traces disabled..
    at PolicyStatement.addPrincipalConditions (.../node_modules/aws-cdk-lib/aws-iam/lib/policy-statement.js:2:7201)
    at PolicyStatement.addPrincipals (.../node_modules/aws-cdk-lib/aws-iam/lib/policy-statement.js:2:2407)
    at new PolicyStatement (.../node_modules/aws-cdk-lib/aws-iam/lib/policy-statement.js:2:663)
    at PolicyStatement.copy (.../node_modules/aws-cdk-lib/aws-iam/lib/policy-statement.js:2:6086)
    at mergeIfCombinable (.../node_modules/aws-cdk-lib/aws-iam/lib/private/merge-statements.js:1:1847)
    at onePass (.../node_modules/aws-cdk-lib/aws-iam/lib/private/merge-statements.js:1:1032)
    at mergeStatements (.../node_modules/aws-cdk-lib/aws-iam/lib/private/merge-statements.js:1:660)
    at PolicyDocument._maybeMergeStatements (.../node_modules/aws-cdk-lib/aws-iam/lib/policy-document.js:1:3033)
    at PolicyDocument.resolve (.../node_modules/aws-cdk-lib/aws-iam/lib/policy-document.js:1:1755)
    at RememberingTokenResolver.resolveToken (.../node_modules/aws-cdk-lib/core/lib/resolvable.js:1:1401)

Reproduction Steps

const principal = new PrincipalWithConditions(
  new ServicePrincipal("scheduler.amazonaws.com"),
  {
    StringEquals: {
      "aws:SourceAccount": Stack.of(this).account,
    },
  }
);

const role = new Role(this, "Role", {
  assumedBy: principal,
});

role.assumeRolePolicy?.addStatements(
  new PolicyStatement({
    effect: Effect.ALLOW,
    principals: [principal],
    actions: ["sts:AssumeRole"],
  })
);

Possible Solution

I think the issue might originate from here main/packages/aws-cdk-lib/aws-iam/lib/private/merge-statements.ts:54, but I can't pinpoint it exactly.

Additional Information/Context

It's worth noting that this issue has downstream effects, impacting the functionality of aws-scheduler-targets when using the same lambda with two schedulers.

const func = new Function(this, "Function", {
  code: Code.fromInline("exports.handler = () => {}"),
  handler: "index.handler",
  runtime: Runtime.NODEJS_18_X,
});

new Schedule(this, "Schedule1", {
  schedule: ScheduleExpression.cron({}),
  target: new LambdaInvoke(func, {}),
});

new Schedule(this, "Schedule2", {
  schedule: ScheduleExpression.cron({}),
  target: new LambdaInvoke(func, {}),
});

CDK CLI Version

2.121.1

Framework Version

No response

Node.js Version

18.19.0

OS

macOS 14.2.1

Language

TypeScript

Language Version

No response

Other information

No response

@Sordie Sordie added bug This issue is a bug. needs-triage This issue or PR still needs to be triaged. labels Jan 15, 2024
@github-actions github-actions bot added the @aws-cdk/aws-iam Related to AWS Identity and Access Management label Jan 15, 2024
@pahud
Copy link
Contributor

pahud commented Jan 16, 2024

Hi

I was not able to reproduce this error with the given snippet:

const principal = new PrincipalWithConditions(
  new ServicePrincipal("scheduler.amazonaws.com"),
  {
    StringEquals: {
      "aws:SourceAccount": Stack.of(this).account,
    },
  }
);

const role = new Role(this, "Role", {
  assumedBy: principal,
});

role.assumeRolePolicy?.addStatements(
  new PolicyStatement({
    effect: Effect.ALLOW,
    principals: [principal],
    actions: ["sts:AssumeRole"],
  })
);

I can synthesize with no error

 "Resources": {
  "Role1ABCC5F0": {
   "Type": "AWS::IAM::Role",
   "Properties": {
    "AssumeRolePolicyDocument": {
     "Statement": [
      {
       "Action": "sts:AssumeRole",
       "Condition": {
        "StringEquals": {
         "aws:SourceAccount": <MY_AWS_ACCOUNT_ID>
        }
       },
       "Effect": "Allow",
       "Principal": {
        "Service": "scheduler.amazonaws.com"
       }
      }
     ],
     "Version": "2012-10-17"
    }
   },
   "Metadata": {
    "aws:cdk:path": "DummyStack/Role/Resource"
   }
  },

And I have the feature flag enabled in cdk.json

    "@aws-cdk/aws-iam:minimizePolicies": true,

@pahud pahud added p2 response-requested Waiting on additional info and feedback. Will move to "closing-soon" in 7 days. effort/medium Medium work item – several days of effort and removed needs-triage This issue or PR still needs to be triaged. labels Jan 16, 2024
@Sordie
Copy link
Author

Sordie commented Jan 17, 2024

Thanks for taking a look. I have created a repository containing a dev container file here. Can you check if you can reproduce the error within the dev container? I have tried different CDK versions and 2.117.0 and older appear to work, while 2.118.0 and higher fail to synth.

@github-actions github-actions bot removed the response-requested Waiting on additional info and feedback. Will move to "closing-soon" in 7 days. label Jan 17, 2024
@dmeehan1968
Copy link

I'm getting the same error when upgrading from CDK 2.110.0 -> 2.126.0 (both CLI and aws-cdk-lib).

The error message is identical to that in the OP, with the exception that mine quotes my AWS account as SourceAccount, rather than the Token directive.

@ssenchenko ssenchenko self-assigned this Feb 8, 2024
@ssenchenko
Copy link

ssenchenko commented Feb 9, 2024

@Sordie thank you so much for providing the repo. I'm able to replicate the error.
btw I replicated an error with v 2.108 so I'm not sure if it goes back before 2.110

@ssenchenko ssenchenko removed their assignment Feb 18, 2024
@wtfzambo
Copy link

Having the same issue exactly when using aws-scheduler-targets

@srtucker
Copy link

@wtfzambo The work around I am using is to create my own role and pass it to all the scheduler targets.

@wtfzambo
Copy link

@wtfzambo The work around I am using is to create my own role and pass it to all the scheduler targets.

@srtucker Roger that. I eventually went back and used L1 constructs.

@Tietew
Copy link
Contributor

Tietew commented Jun 19, 2024

Policy minimizer seems to handle PrincipalWithCondition incorrectly.

Works as expected:

import * as cdk from 'aws-cdk-lib';
import * as iam from 'aws-cdk-lib/aws-iam';

const app = new cdk.App();
const stack = new cdk.Stack(app, 'Stack');
const principal = new iam.ServicePrincipal('dummy.amazonaws.com', {
  conditions: { StringEquals: { 'aws:SourceAccount': stack.account } },
});
const role = new iam.Role(stack, 'Role', { assumedBy: principal });
role.assumeRolePolicy!.addStatements(new iam.PolicyStatement({
  effect: iam.Effect.ALLOW,
  principals: [principal],
  actions: ['sts:AssumeRole'],
}));

Throws Resolution Error:

import * as cdk from 'aws-cdk-lib';
import * as iam from 'aws-cdk-lib/aws-iam';

const app = new cdk.App();
const stack = new cdk.Stack(app, 'Stack');
const principal = new iam.PrincipalWithCondition(new iam.ServicePrincipal('dummy.amazonaws.com'), {
  StringEquals: { 'aws:SourceAccount': stack.account }
});
const role = new iam.Role(stack, 'Role', { assumedBy: principal });
role.assumeRolePolicy!.addStatements(new iam.PolicyStatement({
  effect: iam.Effect.ALLOW,
  principals: [principal],
  actions: ['sts:AssumeRole'],
}));

aws-cdk version is 2.146.0
Above 2 scripts should render same template.

@Tietew
Copy link
Contributor

Tietew commented Jun 24, 2024

I traced the code. The root cause is not in merge phase.
How Role constructor handles assumedBy principal differs from how PolicyDocument.addStatements handles prinipals.

Given

const servicePrincipal = new iam.ServicePrincipal('dummy.amazonaws.com')
const principal = new iam.PrincipalWithConditions(servicePrincipal, { StringEquals: { 'aws:SourceAccount': stack.account } });

The constructor of Role

const role = new iam.Role(stack, 'Role', { assumedBy: principal });

calles:

  1. createAssumeRolePolicy(principal, [])
  2. defaultAddPrincipalsToAssumeRole(principal, new PolicyDocument() /* => doc */)
  3. principal.addToAssumeRolePolicy(doc)
  4. defaultAddPrincipalsToAssumeRole(servicePrincipal, new MutatingPolicyDocumentAdapter(doc, mutator) /* => adapter */)
  5. servicePrincipal.addToAssumeRolePolicy(adapter)
  6. adapter.addStatements(new PolicyStatement({ actions: ..., principals: [servicePrincipal] }) /* => st1 */)
  7. (in PolicyStatement constructor) st1.addPrincipals([servicePrincipal]) // this assigns st1._principals to [servicePrincipal]
  8. mutator(st1)
  9. st1.addActions(principal.assumeRoleAction /* = 'sts:AssumeRole' */
  10. st1.addConditions(principal.conditions /* = { StringEquals: { 'aws:Account': '${Token[...]}' } } 12. doc.addStatements(st1)
  11. role.assumeRolePolicy = doc

At the time, doc.statements[0]._principals is [servicePrincipal] which does not have conditions. doc.statements[0] has.

role.assumeRolePolicy.addStatements(new PolicyStatement({ principals: [principal], ... }) /* => st2 */)

calls:

  1. (in PolicyStatement constructor) st2.addPrincipals([principal]) // this assigns st2._principals to [principal]
  2. (in PolicyStatement constructor) st2.addPrincipalConditions({ StringEquals: { ... } })
  3. doc.statements.push(st2)

At this time, doc.statements[1]._principals is [principal] which has conditions.

in merge phase:

  1. mergeStatements() calls makeComparable for each statement
  2. makeComparable() makes conditionString from JSON.stringify(st.conditions); st1.conditions and st2.conditions are same.
  3. mergeIfCombinable() recognizes st1 and st2 are combinable.
  4. mergeIfCombinable() calls setMergePrincipal() to dedupe principals.
  5. setMergePrincipal() cannot dedupe st1 and st2 because st1._principals[0].dedupeString() is 'ServicePrincipal:dummy.amazonaws.com:{}' and st2._principals[0].dedupeString() is 'PrincipalWithConditions:ServicePrincipal:dummy.amazonaws.com:{}:{"StringEquals":{"aws:SourceAccount":"${Token[...]}"}}'
  6. mergeIfCombinable() calls st1.copy({ principals: [servicePrincipal, principal], ... })
  7. st1.copy() calls new PolicyStatement({ principals: [servicePrincipal, principal], ... }) /* => st3 */
  8. PolicyStatement constructor calls st3.addPrincipals(servicePrincipal, principal)
  9. st3.addPrintipals adds st3.addPrincipalConditions({}) // come from servicePrincipal.conditions is {}
  10. st3.addPrincipals adds st3.addPrincipalConditions({ StringEquals: { ... } }) // come from principal.conditions is { StringEquals: { ... } }
  11. throws Error: All principals in a PolicyStatement must have the same Conditions (got '{}' and '{"StringEquals":{"aws:SourceAccount":"${Token[...]}"}}'). Use multiple statements instead..

@Tietew
Copy link
Contributor

Tietew commented Jun 24, 2024

@pahud Would you please focus this issue now?

mergify bot pushed a commit that referenced this issue Jul 10, 2024
…ror during synth (#30634)

### Reason for this change

Creating multiple `Schedule`s causes Resolution Error during synth.
This PR does not fix the root cause (discussing at #28713), but apply a workaround to prevent the error.

### Description of changes

Use `ServicePrincipal` with conditions directly, instead of `PrincipalWithConditions`.

### Description of how you validated changes

Added a feature flag `{"@aws-cdk/aws-iam:minimizePolicies":true}` to unit tests.
Resolution errors occur before fix. No errors occur after fix.

## Checklist
- [x] My code adheres to the [CONTRIBUTING GUIDE](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md) and [DESIGN GUIDELINES](https://github.com/aws/aws-cdk/blob/main/docs/DESIGN_GUIDELINES.md)

----

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
@NOUIY NOUIY linked a pull request Aug 20, 2024 that will close this issue
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
@aws-cdk/aws-iam Related to AWS Identity and Access Management bug This issue is a bug. effort/medium Medium work item – several days of effort p2
Projects
None yet
7 participants