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(aws-iam): grants support non-identity principals #1623

Merged
merged 40 commits into from
Apr 3, 2019

Conversation

rix0rrr
Copy link
Contributor

@rix0rrr rix0rrr commented Jan 28, 2019

feat(aws-iam): grants support non-identity principals …
Add support for non-identity Principals in grants (for example,
principals that represent accounts or organization IDs). For resources
that support them, the required IAM statements will be added to the
resource policy. For resources that don't support them (because they
don't have resource policies) an error will be thrown.

Add a new OrganizationPrincipal principal which represents all
identities in the given AWS Organization.

Fixes #236.


Pull Request Checklist

  • Testing
    • Unit test added
    • CLI change?: manually run integration tests and paste output as a PR comment
    • cdk-init template change?: coordinated update of integration tests with team
  • Docs
    • jsdocs: All public APIs documented
    • README: README and/or documentation topic updated
  • Title and Description
    • Change type: title prefixed with fix, feat will appear in changelog
    • Title: use lower-case and doesn't end with a period
    • Breaking?: last paragraph: "BREAKING CHANGE: <describe what changed + link for details>"
    • Issues: Indicate issues fixed via: "Fixes #xxx" or "Closes #xxx"
  • Sensitive Modules (requires 2 PR approvers)
    • IAM Policy Document (in @aws-cdk/aws-iam)
    • EC2 Security Groups and ACLs (in @aws-cdk/aws-ec2)
    • Grant APIs (only if not based on official documentation with a reference)

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

Rico Huijbers added 2 commits January 27, 2019 11:33
Add support for non-identity Principals in grants (for example,
principals that represent accounts or organization IDs). For resources
that support them, the required IAM statements will be added to the
resource policy. For resources that don't support them (because they
don't have resource policies) an error will be thrown.

Add a new `OrganizationPrincipal` principal which represents all
identities in the given AWS Organization.

Fixes #236.
@rix0rrr rix0rrr requested a review from a team as a code owner January 28, 2019 12:07
@rix0rrr
Copy link
Contributor Author

rix0rrr commented Jan 28, 2019

Pre-emptive implementation FAQ

Why does IIdentity extends IPrincipal?

So that we can do:

// What we can do today
bucket.grantRead(role);

// As well as (can't do this today)
bucket.grantRead(new AccountPrincipal('12345678'));
bucket.grantRead(new OrganizationPrincipal('o-asdf'));

But now we have to implement assumeRoleAction and policyFragment in all Identity objects!

Because of this inheritance we now have to do:

class Role implements IPrincipal {
    public readonly assumeRoleAction = 'sts:AssumeRole';
    public readonly policyFragment: PolicyFragment;

    constructor(...) {
        // ...
        this.policyFragment = new ArnPrincipal(this.roleArn).policyFragment;
    }
}

Can't we use inheritance? Not really, the ARN comes from a different source
every time. The only thing we could share is the assumeRoleAction, and
declaring 4 abstact methods. Doesn't seem worth it. Repeating the implementation is simpler.

The alternative is the "Curiously Recurring Interface Accessor" pattern, but it's
slightly more complex. I think I actually prefer it, but we keep it simple
unless there is consensus that people don't like simple:

// And also we'd need to find a good name for this.
interface IPrincipalable {
    principal: IPrincipal;
}

interface IPrincipal implements IPrincipalable {
    principal: IPrincipal;
    assumeRoleAction: string;
    policyPrincipal: PolicyPrincipal;
    addToPolicy(...);
}

interface IIdentity extends IPrincipalable {
    principal: IPrincipal;
    addManagedPolicy(...);
}

So that we can do:

class ArnPrincipal implements IPrincipal {
    public readonly principal: IPrincipal = this;
}

class Role implements IPrincipalable {
    public readonly principal: IPrincipal;

    constructor(...) {
        // Slightly better because we get to reuse the whole implementation
        // of ArnPrincipal here.
        this.principal = new ArnPrincipal(this.roleArn);
    }
}

The advantage of doing this is to that we can also do:

class Function implements IPrincipalable {
    public readonly principal: IPrincipal;

    constructor(...) {
        this.principal = this.role;
    }
}

// Instead of
bucket.grantRead(lambda.role);

// We can now do
bucket.grantRead(lambda);

packages/@aws-cdk/aws-iam/lib/grant.ts Outdated Show resolved Hide resolved
packages/@aws-cdk/aws-iam/lib/grant.ts Outdated Show resolved Hide resolved
packages/@aws-cdk/aws-iam/lib/group.ts Show resolved Hide resolved
packages/@aws-cdk/aws-iam/lib/principals.ts Outdated Show resolved Hide resolved
packages/@aws-cdk/aws-cloudwatch/lib/metric.ts Outdated Show resolved Hide resolved
packages/@aws-cdk/aws-dynamodb/lib/table.ts Outdated Show resolved Hide resolved
packages/@aws-cdk/aws-iam/README.md Show resolved Hide resolved
packages/@aws-cdk/aws-iam/lib/grant.ts Outdated Show resolved Hide resolved
packages/@aws-cdk/aws-iam/lib/grant.ts Outdated Show resolved Hide resolved
packages/@aws-cdk/aws-iam/lib/principals.ts Show resolved Hide resolved
packages/@aws-cdk/aws-iam/lib/principals.ts Outdated Show resolved Hide resolved
packages/@aws-cdk/aws-iam/lib/principals.ts Outdated Show resolved Hide resolved
if (!props.principal) { return true; }

const addedToPrincipal = props.principal.addToPolicy(new PolicyStatement()
.addActions(...props.actions)
Copy link
Contributor

Choose a reason for hiding this comment

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

If there are not props.actions, then the statement can (should? must?) be skipped. Same goes for resourceArns

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 alternative is that that's an error. I see what you're saying, but feels like the chance for this to be a mistake is bigger than for it to be an expected use case?

packages/@aws-cdk/aws-iam/lib/grant.ts Outdated Show resolved Hide resolved
@eladb
Copy link
Contributor

eladb commented Feb 4, 2019

@rix0rrr let us know when you want a re-review

@rix0rrr rix0rrr self-assigned this Feb 4, 2019
@rix0rrr
Copy link
Contributor Author

rix0rrr commented Feb 8, 2019

I need to get around to it again. But nothing majorly blocking in the reviews so far it seems.

@fulghum fulghum added the effort/medium Medium work item – several days of effort label Feb 11, 2019
@rix0rrr
Copy link
Contributor Author

rix0rrr commented Feb 13, 2019

Ready for that re-review @eladb @RomainMuller

throw new Error(`Either 'scope' or 'resource' must be supplied.`);
}

// One-iteration loop to be able to skip to end of function easily
Copy link
Contributor

Choose a reason for hiding this comment

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

What's wrong with using an inner function then? 🤷🏻‍♂️ You can return from that whenever you please.

actions,
resourceArns: [
this.tableArn,
new cdk.Token(() => this.hasIndex ? `${this.tableArn}/index/*` : new cdk.Aws().noValue).toString()
Copy link
Contributor

Choose a reason for hiding this comment

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

Rebase

When we say *Principal*, we mean an entity you grant permissions to. This
entity can be an AWS Service, a Role, or something more abstract such as "all
users in this account" or even "all users in this organization". An
*Identity* is an IAM representing a single IAM entity that can have
Copy link
Contributor

Choose a reason for hiding this comment

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

"is an IAM resource representing"

resourceArns: string[];

/**
* Adder to the resource policy
Copy link
Contributor

Choose a reason for hiding this comment

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

Rephrase, it's not an "adder" anymore

* Either 'scope' or 'resource' must be supplied.
*
* An error will be thrown if the policy could not be added to the principal,
* no resource is supplied given and `skipResourcePolicy` is false.
Copy link
Contributor

Choose a reason for hiding this comment

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

"supplied given"

Copy link
Contributor

Choose a reason for hiding this comment

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

Kinda feels like it should be possible to supply only resource without resourceArns somehow. Maybe IResourceWithPolicy can have a property resourceArn which will be the canonic resource ARN to be used?

*
* @default Same as regular resource ARNs
*/
resourceSelfArns?: string[];
Copy link
Contributor

Choose a reason for hiding this comment

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

Wouldn't this make more sense as an enum or boolean?

do {
if (!options.principal) {
// tslint:disable-next-line:max-line-length
scope.node.addWarning(`Could not add grant for '${options.actions}' on '${options.resourceArns}' because the principal was not available. Add the permissions by hand.`);
Copy link
Contributor

Choose a reason for hiding this comment

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

hell yeah!

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 should be a warning, but okay to start

throw new Error(`Either 'scope' or 'resource' must be supplied.`);
}

// One-iteration loop to be able to skip to end of function easily
Copy link
Contributor

Choose a reason for hiding this comment

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

Although this is creative and nice, I rather we keep all our IAM code very simple and straightforward, so it will be dead easy to maintain and reason about.

scope?: cdk.IConstruct;
}

export class Permissions {
Copy link
Contributor

Choose a reason for hiding this comment

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

Something feels wrong with this and I am not sure why. Can we talk? 📲

@rix0rrr
Copy link
Contributor Author

rix0rrr commented Feb 18, 2019

We agreed during a chat to make grant methods output an opaque type which will be produced by our grant method.

Also split out grant method into 2 flavours for the different code paths.

identity.addToPolicy(new iam.PolicyStatement()
.addAllResources()
.addAction("cloudwatch:PutMetricData"));
public static grantPutMetricData(principal?: iam.IPrincipal) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Shouldn't this return a Grant?

@@ -180,11 +181,11 @@ export class Table extends Construct {
*/
public static grantListStreams(principal?: iam.IPrincipal): void {
Copy link
Contributor

Choose a reason for hiding this comment

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

should the awslint rule also be applied to static methods?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Probably. One problem with the statics is that we won't have a scope to attach a warning to.

Copy link
Contributor

Choose a reason for hiding this comment

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

Probably not the end of the world

.addResources(this.tableArn, new cdk.Token(() => this.hasIndex ? `${this.tableArn}/index/*` : new cdk.Aws().noValue).toString())
.addActions(...actions));
public grant(principal?: iam.IPrincipal, ...actions: string[]): iam.GrantResult {
return iam.Permissions.grant({
Copy link
Contributor

Choose a reason for hiding this comment

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

I was under the impression that we wanted a specific API for resources with resource policies, no?

@rix0rrr rix0rrr requested a review from eladb March 4, 2019 14:06
]
]
}
},
{
"Action": [
Copy link
Contributor

Choose a reason for hiding this comment

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

Does it make sense that we lost these permissions?

Copy link
Contributor Author

@rix0rrr rix0rrr Apr 3, 2019

Choose a reason for hiding this comment

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

Yes, the user pulling from ECR doesn't need to create logs (they should have never have been part of that grant in the first place).

packages/@aws-cdk/aws-cloudwatch/lib/metric.ts Outdated Show resolved Hide resolved
packages/@aws-cdk/aws-codebuild/lib/project.ts Outdated Show resolved Hide resolved
packages/@aws-cdk/aws-codebuild/lib/project.ts Outdated Show resolved Hide resolved
@rix0rrr
Copy link
Contributor Author

rix0rrr commented Apr 3, 2019

NuGet build error again. Seems to be getting worse?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
contribution/core This is a PR that came from AWS. effort/medium Medium work item – several days of effort
Projects
None yet
Development

Successfully merging this pull request may close these issues.

iam: Grants only work on IAM objects, not arbitrary principals
5 participants