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

feat(iam): Validate the policy statements when synthesizing policy #7897

Closed
wants to merge 9 commits into from

Conversation

shreyasdamle
Copy link
Contributor

Commit Message

Added compile time validation for the policy statements. PolicyStatement props will compile only with the following condition:

export type PolicyStatementProps =  (PolicyStatementActions | PolicyStatementNotActions) & (PolicyStatementResources | PolicyStatementNotResources);

Fixes #7615

End Commit Message


By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license

@aws-cdk-automation
Copy link
Collaborator

AWS CodeBuild CI Report

  • CodeBuild project: AutoBuildProject6AEA49D1-qxepHUsryhcu
  • Commit ID: e41f22e
  • Result: FAILED
  • Build Logs (available for 30 days)

Powered by github-codebuild-logs, available on the AWS Serverless Application Repository

@aws-cdk-automation
Copy link
Collaborator

AWS CodeBuild CI Report

  • CodeBuild project: AutoBuildProject6AEA49D1-qxepHUsryhcu
  • Commit ID: c5aa2b2
  • Result: FAILED
  • Build Logs (available for 30 days)

Powered by github-codebuild-logs, available on the AWS Serverless Application Repository

@@ -33,8 +33,8 @@ export class PolicyStatement {
resources: ensureArrayOrUndefined(obj.Resource),
conditions: obj.Condition,
effect: obj.Effect,
notActions: ensureArrayOrUndefined(obj.NotAction),
notResources: ensureArrayOrUndefined(obj.NotResource),
notActions: ensureArrayOrUndefined(obj.NotAction) ?? [],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why this change?

Why this change for notActions and notResources, and not for actions and resources?

readonly notResources: string[];
}

export type PolicyStatementProps = (PolicyStatementActions | PolicyStatementNotActions) & (PolicyStatementResources | PolicyStatementNotResources);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can't support type declarations like this in jsii.

Your build should have yelled at you.

resources: ['abc'],
notResources: ['def'],
notResources: ['abc'],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should not have needed to change this line. If you have, something is wrong.

@mergify mergify bot dismissed rix0rrr’s stale review May 19, 2020 01:46

Pull request has been modified.

@aws-cdk-automation
Copy link
Collaborator

AWS CodeBuild CI Report

  • CodeBuild project: AutoBuildProject6AEA49D1-qxepHUsryhcu
  • Commit ID: e4470b2
  • Result: FAILED
  • Build Logs (available for 30 days)

Powered by github-codebuild-logs, available on the AWS Serverless Application Repository

@aws-cdk-automation
Copy link
Collaborator

AWS CodeBuild CI Report

  • CodeBuild project: AutoBuildProject6AEA49D1-qxepHUsryhcu
  • Commit ID: 8c520a9
  • Result: FAILED
  • Build Logs (available for 30 days)

Powered by github-codebuild-logs, available on the AWS Serverless Application Repository

@aws-cdk-automation
Copy link
Collaborator

AWS CodeBuild CI Report

  • CodeBuild project: AutoBuildProject6AEA49D1-qxepHUsryhcu
  • Commit ID: 806062c
  • Result: FAILED
  • Build Logs (available for 30 days)

Powered by github-codebuild-logs, available on the AWS Serverless Application Repository

@aws-cdk-automation
Copy link
Collaborator

AWS CodeBuild CI Report

  • CodeBuild project: AutoBuildProject6AEA49D1-qxepHUsryhcu
  • Commit ID: 507f493
  • Result: FAILED
  • Build Logs (available for 30 days)

Powered by github-codebuild-logs, available on the AWS Serverless Application Repository

@aws-cdk-automation
Copy link
Collaborator

AWS CodeBuild CI Report

  • CodeBuild project: AutoBuildProject6AEA49D1-qxepHUsryhcu
  • Commit ID: b552ba7
  • Result: FAILED
  • Build Logs (available for 30 days)

Powered by github-codebuild-logs, available on the AWS Serverless Application Repository

@aws-cdk-automation
Copy link
Collaborator

AWS CodeBuild CI Report

  • CodeBuild project: AutoBuildProject6AEA49D1-qxepHUsryhcu
  • Commit ID: 8befbe9
  • Result: FAILED
  • Build Logs (available for 30 days)

Powered by github-codebuild-logs, available on the AWS Serverless Application Repository

@aws-cdk-automation
Copy link
Collaborator

AWS CodeBuild CI Report

  • CodeBuild project: AutoBuildProject6AEA49D1-qxepHUsryhcu
  • Commit ID: 8ba9a3b
  • Result: FAILED
  • Build Logs (available for 30 days)

Powered by github-codebuild-logs, available on the AWS Serverless Application Repository

@aws-cdk-automation
Copy link
Collaborator

AWS CodeBuild CI Report

  • CodeBuild project: AutoBuildProject6AEA49D1-qxepHUsryhcu
  • Commit ID: aac74dc
  • Result: FAILED
  • Build Logs (available for 30 days)

Powered by github-codebuild-logs, available on the AWS Serverless Application Repository

@aws-cdk-automation
Copy link
Collaborator

AWS CodeBuild CI Report

  • CodeBuild project: AutoBuildProject6AEA49D1-qxepHUsryhcu
  • Commit ID: d0081f9
  • Result: FAILED
  • Build Logs (available for 30 days)

Powered by github-codebuild-logs, available on the AWS Serverless Application Repository

@aws-cdk-automation
Copy link
Collaborator

AWS CodeBuild CI Report

  • CodeBuild project: AutoBuildProject6AEA49D1-qxepHUsryhcu
  • Commit ID: b70dacd
  • Result: FAILED
  • Build Logs (available for 30 days)

Powered by github-codebuild-logs, available on the AWS Serverless Application Repository

@aws-cdk-automation
Copy link
Collaborator

AWS CodeBuild CI Report

  • CodeBuild project: AutoBuildProject6AEA49D1-qxepHUsryhcu
  • Commit ID: f37637b
  • Result: FAILED
  • Build Logs (available for 30 days)

Powered by github-codebuild-logs, available on the AWS Serverless Application Repository

@aws-cdk-automation
Copy link
Collaborator

AWS CodeBuild CI Report

  • CodeBuild project: AutoBuildProject6AEA49D1-qxepHUsryhcu
  • Commit ID: 4e743ed
  • Result: FAILED
  • Build Logs (available for 30 days)

Powered by github-codebuild-logs, available on the AWS Serverless Application Repository

* @param props
*/
private validateProps(props: PolicyStatementProps) {
if (props.conditions || props.effect || props.notPrincipals || props.notResources || props.principals || props.resources || props.sid) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't quite get this additional guarding if. Is action always required, or not? Where are you running into trouble if you don't add this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The additional if is to guard against PolicyStatement class instantiated without any props.

const p = new PolicyStatement();

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah the old usage pattern. Good catch. Could you rewrite it a little though to make that more self-evident for the future?

Maybe to the following:

const anyConstructorParamsSupplied = Object.values(props).filter(x => x !== undefined).length > 0;

if (anyConstructorParamsSupplied && !(props.actions || props.notActions)) {
  throw new Error('...');
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey but while writing that, that reminds me...

We can force either a full policy statement in the constructor, or an empty policy statement in the constructor and then using the fluent API... but why would we prevent a mix?

I think it's actually a breaking change to do that.

People could have written:

const s = new PolicyStatement({ resources: ['*'] });
s.addPrincipal(...):

And this change would break them.

The only place where we can do proper validation is at the end, when we are rendering the policy statement out.

@@ -249,6 +249,18 @@ const newPolicyDocument = PolicyDocument.fromJson(policyDocument);

```

### Policy Statement

As per the doc of [IAM policy grammar](https://docs.aws.amazon.com/en_us/IAM/latest/UserGuide/reference_policies_grammar.html#policies-grammar-bnf), the action and resource blocks are mandatory. Hence, the new policy stament using `PolicyStatementProps` can be created as:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure this README section is helping a lot. What kind of information do you think people are missing?

Or did you write this section just to appease the linter?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I just added it to appease the linter. Is there any better way to handle this?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ha, yes, we can add a tag to silence it.

}
}
if (props.actions || props.conditions || props.effect || props.notActions || props.notPrincipals || props.principals || props.sid) {
if (!props.resources && !props.notResources) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also have a question on IAM policy grammar. It says the action and resource blocks are mandatory. However, for certain policies (example: trust relationship policy) resource block isn't allowed. Here's the instance of such case:

 for (const a of input.lambdaFunctionAssociations) {
        if (a.lambdaFunction.role && a.lambdaFunction.role instanceof iam.Role && a.lambdaFunction.role.assumeRolePolicy) {
          a.lambdaFunction.role.assumeRolePolicy.addStatements(new iam.PolicyStatement({
            actions: [ 'sts:AssumeRole' ],
            principals: [ new iam.ServicePrincipal('edgelambda.amazonaws.com') ],
          }));
        }
      }

Please let me know if I'm missing something. I will move this PR to draft in the meantime.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docs aren't super clear about this, but there is a distinction between:

  • Identity policies (what permissions an actor is allowed to perform, generally called IAM policies)
  • Resource policies (what actions does the resource allow to be performed TO IT, you can think of these as "trust" policies although that is not a name that is in general use).

The syntax between these two is quite similar, except:

  • An identity policy generally does not need to indicate the principal, as it is always about this principal.
  • A resource policy generally does not need to indicate the resource, as it is always about this resource (of course there are exceptions: some resources like Queues require a *, and some resources like Buckets allow you to use ARNs to identify objects in the bucket).

Now here's the catch:

An AssumeRolePolicy is actually a resource policy on an identity object.

You can see that that's true, because the AssumeRolePolicy defines what actions are allowed to performed on the role (very specifically, who is allowed to Assume it).

And so therefore, it does not take a Resource. The resource is implicitly: this role.

@shreyasdamle shreyasdamle marked this pull request as draft June 10, 2020 20:35
@rix0rrr rix0rrr added the pr-linter/exempt-readme The PR linter will not require README changes label Jun 19, 2020
* @param props
*/
private validateProps(props: PolicyStatementProps) {
if (props.conditions || props.effect || props.notPrincipals || props.notResources || props.principals || props.resources || props.sid) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah the old usage pattern. Good catch. Could you rewrite it a little though to make that more self-evident for the future?

Maybe to the following:

const anyConstructorParamsSupplied = Object.values(props).filter(x => x !== undefined).length > 0;

if (anyConstructorParamsSupplied && !(props.actions || props.notActions)) {
  throw new Error('...');
}

* @param props
*/
private validateProps(props: PolicyStatementProps) {
if (props.conditions || props.effect || props.notPrincipals || props.notResources || props.principals || props.resources || props.sid) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey but while writing that, that reminds me...

We can force either a full policy statement in the constructor, or an empty policy statement in the constructor and then using the fluent API... but why would we prevent a mix?

I think it's actually a breaking change to do that.

People could have written:

const s = new PolicyStatement({ resources: ['*'] });
s.addPrincipal(...):

And this change would break them.

The only place where we can do proper validation is at the end, when we are rendering the policy statement out.

@@ -925,6 +925,7 @@ export class CloudFrontWebDistribution extends cdk.Construct implements IDistrib
if (a.lambdaFunction.role && a.lambdaFunction.role instanceof iam.Role && a.lambdaFunction.role.assumeRolePolicy) {
a.lambdaFunction.role.assumeRolePolicy.addStatements(new iam.PolicyStatement({
actions: [ 'sts:AssumeRole' ],
resources: ['*'],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah as you mentioned yourself, this is incorrect. This * doesn't go here.

}
}
if (props.actions || props.conditions || props.effect || props.notActions || props.notPrincipals || props.principals || props.sid) {
if (!props.resources && !props.notResources) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docs aren't super clear about this, but there is a distinction between:

  • Identity policies (what permissions an actor is allowed to perform, generally called IAM policies)
  • Resource policies (what actions does the resource allow to be performed TO IT, you can think of these as "trust" policies although that is not a name that is in general use).

The syntax between these two is quite similar, except:

  • An identity policy generally does not need to indicate the principal, as it is always about this principal.
  • A resource policy generally does not need to indicate the resource, as it is always about this resource (of course there are exceptions: some resources like Queues require a *, and some resources like Buckets allow you to use ARNs to identify objects in the bucket).

Now here's the catch:

An AssumeRolePolicy is actually a resource policy on an identity object.

You can see that that's true, because the AssumeRolePolicy defines what actions are allowed to performed on the role (very specifically, who is allowed to Assume it).

And so therefore, it does not take a Resource. The resource is implicitly: this role.

@rix0rrr
Copy link
Contributor

rix0rrr commented Aug 10, 2020

Seems to have been replaced by #9269

@rix0rrr rix0rrr closed this Aug 10, 2020
mergify bot pushed a commit that referenced this pull request Aug 11, 2020
Adds policy statement validation checks for all constructs that contain IAM policies.

Fixes #7615

----

Note: sensitive module (requires 2 PR approvers). I am not sure if the changes presented here are considered breaking or not.

I'm looking to get early feedback for this commit! I'm not an IAM subject matter expert, so this is my best interpretation of the AWS docs I've read so far - but I'm still trying to figure out what details and edge cases I'm missing if any. 😅

## Problem

To recap what rix0rrr mentions in <#7897>, my understanding is that there are two main kinds of IAM policies we are interested in checking:

- identity-based policy: gets **attached to an IAM principal**, but **specifies the resources** it applies to
- resource-based policy gets **attached to a resource**, but **specifies the IAM principal(s)** it applies to

I am not aware of any other kinds of policies mentioned in the AWS docs [here](https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies.html) that can be constructed in the CDK.

Based on [this](https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies.html#access_policies-json) IAM docs page, it seems to me that there are at least four types of distinct policy statements errors that we could feasibly try to check right before synthesis time:

1. a resource-based policy and no principal/notPrincipal are specified
2. a identity-based policy and principal/notPrincipal are specified
3. a identity-based policy and no resources/notResources are specified
4. any kind of policy and no actions/notActions are specified

The reason we need to perform these checks right before synthesis is because it can't be done at constructor time, since CDK users can add actions, principals, etc. after creation / out of order.

## Design

Since we want the validation logic to happen at synthesis time, the best option is to override the validate() method for the appropriate Construct child subclasses that we want these rules to apply to. The following is the list of constructs I found that directly contain policy documents:

- Role
- ManagedPolicy
- Policy
- BucketPolicy
- TopicPolicy
- QueuePolicy
- Repository (ECR)
- Secret
- Key
- Alias (KMS)

All other constructs that have some kind of role/policy statement functions (e.g. iam.User, lambda.Function) automatically get the validations transitively through one of the constructs above.

In my commits, I've added appropriate methods to PolicyStatement and PolicyDocument to perform validation of various kinds, and then called these methods in the appropriate construct's validate() methods.

### Other considerations:

It's also possible we could add calls to the PolicyStatement validation methods inside other methods such as addToResourcePolicy() found within several classes - but I think this could make the library less flexible, since a statement can still be modified after it has been added to a PolicyDocument (correct me if wrong).

An alternative design I considered for the case of Resource constructs was extending the interface of IResourceWithPolicy with a validatePolicy() method, but it doesn't make a lot of sense to make the method public (which would have been required by TypeScript), and in the end all I want to do is overwrite the protected Construct#validate() method anyway. Since IResourceWithPolicy is just an interface (and not a class), I don't see any way to cleanly enforce that all Resources with policies will perform the policy validation, but I think the current solution is adequate.

## Examples

These are examples of the four errors presented above that you can easily verify will fail during deployment (but aren't caught at compile or synth time), and which I've tested fail with my added changes.

```
// 1
const bucket = new s3.Bucket(this, 'TestBucket');
bucket.addToResourcePolicy(new iam.PolicyStatement({
  actions: ['*']
}));

// 2
const role = new iam.Role(this, 'TestRole', {
  assumedBy: new iam.AnyPrincipal(),
})
role.attachInlinePolicy(new iam.Policy(this, 'MyPolicy', {
  statements: [new iam.PolicyStatement({
    resources: ['*'],
    actions: ['*'],
    principals: [new iam.AnyPrincipal()]
  })]
}));

// 3
const role = new iam.Role(this, 'TestRole', {
  assumedBy: new iam.AccountPrincipal("457310432007"),
});
role.attachInlinePolicy(new iam.Policy(this, 'MyPolicy', {
  statements: [new iam.PolicyStatement({
    actions: ['*'],
  })]
}));

// 4
const bucket = new s3.Bucket(this, 'TestBucket')
bucket.addToResourcePolicy(new iam.PolicyStatement({
  principals: [new iam.AnyPrincipal()]
}));
```

## Issues

- [x] Policy documents added through the "inlinePolicies" prop of iam.Role do not get validated.
- [x] Add blurb about feature to [README.md](https://github.com/aws/aws-cdk/blob/master/packages/%40aws-cdk/aws-iam/README.md) to silence PR linter

----

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
pr-linter/exempt-readme The PR linter will not require README changes
Projects
None yet
Development

Successfully merging this pull request may close these issues.

iam: Validate the policy statements when synthesizing policy
3 participants