Skip to content

Commit

Permalink
feat(aws-iam): grants support non-identity principals
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
Rico Huijbers committed Jan 28, 2019
1 parent 19ad316 commit be0ec5d
Show file tree
Hide file tree
Showing 21 changed files with 266 additions and 121 deletions.
Expand Up @@ -344,9 +344,10 @@ class RoleDouble extends iam.Role {
super(scope, id, props);
}

public addToPolicy(statement: iam.PolicyStatement) {
public addToPolicy(statement: iam.PolicyStatement): boolean {
super.addToPolicy(statement);
this.statements.push(statement);
return true;
}
}

Expand Down
14 changes: 7 additions & 7 deletions packages/@aws-cdk/aws-cloudwatch/lib/metric.ts
Expand Up @@ -85,14 +85,14 @@ export class Metric {
/**
* Grant permissions to the given identity to write metrics.
*
* @param identity The IAM identity to give permissions to.
* @param principal The IAM identity to give permissions to.
*/
public static grantPutMetricData(identity?: iam.IPrincipal) {
if (!identity) { return; }

identity.addToPolicy(new iam.PolicyStatement()
.addAllResources()
.addAction("cloudwatch:PutMetricData"));
public static grantPutMetricData(principal?: iam.IPrincipal) {
iam.grant({
principal,
actions: ['cloudwatch:PutMetricData'],
resourceArns: ['*'],
});
}

public readonly dimensions?: DimensionHash;
Expand Down
37 changes: 19 additions & 18 deletions packages/@aws-cdk/aws-dynamodb/lib/table.ts
Expand Up @@ -185,12 +185,12 @@ export class Table extends Construct {
* @param principal The principal (no-op if undefined)
*/
public static grantListStreams(principal?: iam.IPrincipal): void {
if (principal) {
principal.addToPolicy(new iam.PolicyStatement()
.addAction('dynamodb:ListStreams')
.addResource("*"));
}
}
iam.grant({
principal,
actions: ['dynamodb:ListStreams'],
resourceArns: ['*'],
});
}

public readonly tableArn: string;
public readonly tableName: string;
Expand Down Expand Up @@ -443,12 +443,14 @@ export class Table extends Construct {
* @param actions The set of actions to allow (i.e. "dynamodb:PutItem", "dynamodb:GetItem", ...)
*/
public grant(principal?: iam.IPrincipal, ...actions: string[]) {
if (!principal) {
return;
}
principal.addToPolicy(new iam.PolicyStatement()
.addResources(this.tableArn, new cdk.Token(() => this.hasIndex ? `${this.tableArn}/index/*` : new cdk.Aws().noValue).toString())
.addActions(...actions));
iam.grant({
principal,
actions,
resourceArns: [
this.tableArn,
new cdk.Token(() => this.hasIndex ? `${this.tableArn}/index/*` : new cdk.Aws().noValue).toString()
]
});
}

/**
Expand All @@ -458,12 +460,11 @@ export class Table extends Construct {
* @param actions The set of actions to allow (i.e. "dynamodb:DescribeStream", "dynamodb:GetRecords", ...)
*/
public grantStream(principal?: iam.IPrincipal, ...actions: string[]) {
if (!principal) {
return;
}
principal.addToPolicy(new iam.PolicyStatement()
.addResource(this.tableStreamArn)
.addActions(...actions));
iam.grant({
principal,
actions,
resourceArns: [this.tableStreamArn]
});
}

/**
Expand Down
31 changes: 16 additions & 15 deletions packages/@aws-cdk/aws-ecr/lib/repository-ref.ts
Expand Up @@ -212,26 +212,27 @@ export abstract class RepositoryBase extends cdk.Construct implements IRepositor
/**
* Grant the given principal identity permissions to perform the actions on this repository
*/
public grant(identity?: iam.IPrincipal, ...actions: string[]) {
if (!identity) {
return;
}
identity.addToPolicy(new iam.PolicyStatement()
.addResource(this.repositoryArn)
.addActions(...actions));
public grant(principal?: iam.IPrincipal, ...actions: string[]) {
iam.grant({
principal,
actions,
resourceArns: [this.repositoryArn],
addToResourcePolicy: this.addToResourcePolicy.bind(this),
});
}

/**
* Grant the given identity permissions to use the images in this repository
*/
public grantPull(identity?: iam.IPrincipal) {
this.grant(identity, "ecr:BatchCheckLayerAvailability", "ecr:GetDownloadUrlForLayer", "ecr:BatchGetImage");

if (identity) {
identity.addToPolicy(new iam.PolicyStatement()
.addActions("ecr:GetAuthorizationToken", "logs:CreateLogStream", "logs:PutLogEvents")
.addAllResources());
}
public grantPull(principal?: iam.IPrincipal) {
this.grant(principal, "ecr:BatchCheckLayerAvailability", "ecr:GetDownloadUrlForLayer", "ecr:BatchGetImage");

iam.grant({
principal,
actions: ["ecr:GetAuthorizationToken", "logs:CreateLogStream", "logs:PutLogEvents"],
resourceArns: ['*'],
skipResourcePolicy: true,
});
}

/**
Expand Down
3 changes: 2 additions & 1 deletion packages/@aws-cdk/aws-iam/lib/group.ts
Expand Up @@ -97,12 +97,13 @@ export class Group extends Construct implements IIdentity {
/**
* Adds an IAM statement to the default policy.
*/
public addToPolicy(statement: PolicyStatement) {
public addToPolicy(statement: PolicyStatement): boolean {
if (!this.defaultPolicy) {
this.defaultPolicy = new Policy(this, 'DefaultPolicy');
this.defaultPolicy.attachToGroup(this);
}

this.defaultPolicy.addStatement(statement);
return true;
}
}
14 changes: 2 additions & 12 deletions packages/@aws-cdk/aws-iam/lib/identity-base.ts
@@ -1,7 +1,6 @@
import cdk = require('@aws-cdk/cdk');
import { Policy } from "./policy";
import { PolicyStatement } from "./policy-document";
import { IPrincipal, PrincipalPolicyFragment } from "./principals";
import { IPrincipal } from "./principals";

/**
* A construct that represents an IAM principal, such as a user, group or role.
Expand All @@ -11,7 +10,7 @@ export interface IIdentity extends IPrincipal {
* Adds an IAM statement to the default inline policy associated with this
* principal. If a policy doesn't exist, it is created.
*/
addToPolicy(statement: PolicyStatement): void;
addToPolicy(statement: PolicyStatement): boolean;

/**
* Attaches an inline policy to this principal.
Expand All @@ -25,13 +24,4 @@ export interface IIdentity extends IPrincipal {
* @param arn The ARN of the managed policy
*/
attachManagedPolicy(arn: string): void;
}

export abstract class IdentityBase extends cdk.Construct implements IIdentity {
public readonly assumeRoleAction: string = 'sts:AssumeRole';

public abstract policyFragment: PrincipalPolicyFragment;
public abstract addToPolicy(statement: PolicyStatement): void;
public abstract attachInlinePolicy(policy: Policy): void;
public abstract attachManagedPolicy(arn: string): void;
}
1 change: 1 addition & 0 deletions packages/@aws-cdk/aws-iam/lib/index.ts
Expand Up @@ -7,6 +7,7 @@ export * from './group';
export * from './lazy-role';
export * from './principals';
export * from './identity-base';
export * from './grant';

// AWS::IAM CloudFormation Resources:
export * from './iam.generated';
16 changes: 8 additions & 8 deletions packages/@aws-cdk/aws-iam/lib/lazy-role.ts
@@ -1,6 +1,7 @@
import cdk = require('@aws-cdk/cdk');
import { Policy } from './policy';
import { PolicyPrincipal, PolicyStatement } from './policy-document';
import { PolicyStatement } from './policy-document';
import { PrincipalPolicyFragment } from './principals';
import { IRole, Role, RoleImportProps, RoleProps } from './role';

/**
Expand All @@ -13,6 +14,7 @@ import { IRole, Role, RoleImportProps, RoleProps } from './role';
* not be synthesized or deployed.
*/
export class LazyRole extends cdk.Construct implements IRole {
public readonly assumeRoleAction: string = 'sts:AssumeRole';
private role?: Role;
private readonly statements = new Array<PolicyStatement>();
private readonly policies = new Array<Policy>();
Expand All @@ -31,11 +33,12 @@ export class LazyRole extends cdk.Construct implements IRole {
* If there is no default policy attached to this role, it will be created.
* @param permission The permission statement to add to the policy document
*/
public addToPolicy(statement: PolicyStatement): void {
public addToPolicy(statement: PolicyStatement): boolean {
if (this.role) {
this.role.addToPolicy(statement);
return this.role.addToPolicy(statement);
} else {
this.statements.push(statement);
return true;
}
}

Expand Down Expand Up @@ -85,11 +88,8 @@ export class LazyRole extends cdk.Construct implements IRole {
return this.instantiate().roleName;
}

/**
* Returns a Principal object representing the ARN of this role.
*/
public get principal(): PolicyPrincipal {
return this.instantiate().principal;
public get policyFragment(): PrincipalPolicyFragment {
return this.instantiate().policyFragment;
}

private instantiate(): Role {
Expand Down
29 changes: 29 additions & 0 deletions packages/@aws-cdk/aws-iam/lib/principals.ts
@@ -1,4 +1,5 @@
import cdk = require('@aws-cdk/cdk');
import { PolicyStatement } from './policy-document';
import { mergePrincipal } from './util';

/**
Expand All @@ -16,6 +17,13 @@ export interface IPrincipal {
* Return the policy fragment that identifies this principal in a Policy.
*/
readonly policyFragment: PrincipalPolicyFragment;

/**
* Add to the policy of this principal.
*
* @returns true if the policy was added, false if the policy could not be added
*/
addToPolicy(statement: PolicyStatement): boolean;
}

/**
Expand All @@ -31,6 +39,11 @@ export abstract class PrincipalBase implements IPrincipal {
* Return the policy fragment that identifies this principal in a Policy.
*/
public abstract policyFragment: PrincipalPolicyFragment;

public addToPolicy(_statement: PolicyStatement): boolean {
// None of these have a policy to add to
return false;
}
}

/**
Expand Down Expand Up @@ -75,6 +88,22 @@ export class ServicePrincipal extends PrincipalBase {
}
}

/**
* A principal that represents an AWS Organization
*/
export class OrganizationPrincipal extends PrincipalBase {
constructor(public readonly organizationId: string) {
super();
}

public get policyFragment(): PrincipalPolicyFragment {
return new PrincipalPolicyFragment(
{ AWS: ['*'] },
{ StringEquals: { 'aws:PrincipalOrgID': this.organizationId } }
);
}
}

/**
* A policy prinicipal for canonicalUserIds - useful for S3 bucket policies that use
* Origin Access identities.
Expand Down
8 changes: 5 additions & 3 deletions packages/@aws-cdk/aws-iam/lib/role.ts
Expand Up @@ -182,13 +182,14 @@ export class Role extends Construct implements IRole {
* If there is no default policy attached to this role, it will be created.
* @param permission The permission statement to add to the policy document
*/
public addToPolicy(statement: PolicyStatement) {
public addToPolicy(statement: PolicyStatement): boolean {
if (!this.defaultPolicy) {
this.defaultPolicy = new Policy(this, 'DefaultPolicy');
this.attachInlinePolicy(this.defaultPolicy);
this.dependencyElements.push(this.defaultPolicy);
}
this.defaultPolicy.addStatement(statement);
return true;
}

/**
Expand Down Expand Up @@ -310,8 +311,9 @@ class ImportedRole extends Construct implements IRole {
return this.props;
}

public addToPolicy(_statement: PolicyStatement): void {
// FIXME: Add warning that we're ignoring this
public addToPolicy(_statement: PolicyStatement): boolean {
// Statement will be added to resource instead
return false;
}

public attachInlinePolicy(_policy: Policy): void {
Expand Down
3 changes: 2 additions & 1 deletion packages/@aws-cdk/aws-iam/lib/user.ts
Expand Up @@ -130,13 +130,14 @@ export class User extends Construct implements IIdentity {
/**
* Adds an IAM statement to the default policy.
*/
public addToPolicy(statement: PolicyStatement) {
public addToPolicy(statement: PolicyStatement): boolean {
if (!this.defaultPolicy) {
this.defaultPolicy = new Policy(this, 'DefaultPolicy');
this.defaultPolicy.attachToUser(this);
}

this.defaultPolicy.addStatement(statement);
return true;
}

private parseLoginProfile(props: UserProps): CfnUser.LoginProfileProperty | undefined {
Expand Down
1 change: 1 addition & 0 deletions packages/@aws-cdk/aws-iam/test/test.policy-document.ts
Expand Up @@ -281,6 +281,7 @@ export = {
const arrayPrincipal: PrincipalBase = {
assumeRoleAction: 'sts:AssumeRole',
policyFragment: new PrincipalPolicyFragment({ AWS: ['foo', 'bar'] }),
addToPolicy() { return false; }
};
const s = new PolicyStatement().addAccountRootPrincipal()
.addPrincipal(arrayPrincipal);
Expand Down
6 changes: 3 additions & 3 deletions packages/@aws-cdk/aws-kinesis/lib/stream.ts
Expand Up @@ -255,14 +255,14 @@ export abstract class StreamBase extends cdk.Construct implements IStream {
return dest.logSubscriptionDestination(sourceLogGroup);
}

private grant(identity: iam.IPrincipal, actions: { streamActions: string[], keyActions: string[] }) {
identity.addToPolicy(new iam.PolicyStatement()
private grant(principal: iam.IPrincipal, actions: { streamActions: string[], keyActions: string[] }) {
principal.addToPolicy(new iam.PolicyStatement()
.addResource(this.streamArn)
.addActions(...actions.streamActions));

// grant key permissions if there's an associated key.
if (this.encryptionKey) {
identity.addToPolicy(new iam.PolicyStatement()
principal.addToPolicy(new iam.PolicyStatement()
.addResource(this.encryptionKey.keyArn)
.addActions(...actions.keyActions));
}
Expand Down

0 comments on commit be0ec5d

Please sign in to comment.