Skip to content

Commit

Permalink
Introduce Role -> IIdentity -> IPrincipal
Browse files Browse the repository at this point in the history
  • Loading branch information
Rico Huijbers committed Jan 27, 2019
1 parent 876b26d commit 19ad316
Show file tree
Hide file tree
Showing 11 changed files with 291 additions and 255 deletions.
8 changes: 8 additions & 0 deletions packages/@aws-cdk/aws-iam/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,14 @@ an `ExternalId` works like this:

[supplying an external ID](test/example.external-id.lit.ts)

### Principals vs Identities

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
a policy attached, one of `Role`, `User`, or `Group`.

### IAM Principals

When defining policy statements as part of an AssumeRole policy or as part of a
Expand Down
16 changes: 8 additions & 8 deletions packages/@aws-cdk/aws-iam/lib/group.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { Construct } from '@aws-cdk/cdk';
import { CfnGroup } from './iam.generated';
import { IPrincipal, Policy } from './policy';
import { ArnPrincipal, PolicyPrincipal, PolicyStatement } from './policy-document';
import { IIdentity } from './identity-base';
import { Policy } from './policy';
import { PolicyStatement } from './policy-document';
import { ArnPrincipal, PrincipalPolicyFragment } from './principals';
import { User } from './user';
import { AttachedPolicies, undefinedIfEmpty } from './util';

Expand Down Expand Up @@ -34,7 +36,8 @@ export interface GroupProps {
path?: string;
}

export class Group extends Construct implements IPrincipal {
export class Group extends Construct implements IIdentity {
public readonly assumeRoleAction: string = 'sts:AssumeRole';
/**
* The runtime name of this group.
*/
Expand All @@ -45,10 +48,7 @@ export class Group extends Construct implements IPrincipal {
*/
public readonly groupArn: string;

/**
* An "AWS" policy principal that represents this group.
*/
public readonly principal: PolicyPrincipal;
public readonly policyFragment: PrincipalPolicyFragment;

private readonly managedPolicies: any[];
private readonly attachedPolicies = new AttachedPolicies();
Expand All @@ -67,7 +67,7 @@ export class Group extends Construct implements IPrincipal {

this.groupName = group.groupName;
this.groupArn = group.groupArn;
this.principal = new ArnPrincipal(this.groupArn);
this.policyFragment = new ArnPrincipal(this.groupArn).policyFragment;
}

/**
Expand Down
37 changes: 37 additions & 0 deletions packages/@aws-cdk/aws-iam/lib/identity-base.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import cdk = require('@aws-cdk/cdk');
import { Policy } from "./policy";
import { PolicyStatement } from "./policy-document";
import { IPrincipal, PrincipalPolicyFragment } from "./principals";

/**
* A construct that represents an IAM principal, such as a user, group or role.
*/
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;

/**
* Attaches an inline policy to this principal.
* This is the same as calling `policy.addToXxx(principal)`.
* @param policy The policy resource to attach to this principal.
*/
attachInlinePolicy(policy: Policy): void;

/**
* Attaches a managed policy to this principal.
* @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;
}
2 changes: 2 additions & 0 deletions packages/@aws-cdk/aws-iam/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ export * from './policy';
export * from './user';
export * from './group';
export * from './lazy-role';
export * from './principals';
export * from './identity-base';

// AWS::IAM CloudFormation Resources:
export * from './iam.generated';
192 changes: 5 additions & 187 deletions packages/@aws-cdk/aws-iam/lib/policy-document.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import cdk = require('@aws-cdk/cdk');
import { AccountPrincipal, AccountRootPrincipal, Anyone,
ArnPrincipal, CanonicalUserPrincipal, FederatedPrincipal, IPrincipal, ServicePrincipal } from './principals';
import { mergePrincipal } from './util';

export class PolicyDocument extends cdk.Token {
private statements = new Array<PolicyStatement>();
Expand Down Expand Up @@ -42,162 +45,6 @@ export class PolicyDocument extends cdk.Token {
}
}

/**
* Represents an IAM principal.
*/
export abstract class PolicyPrincipal {
/**
* When this Principal is used in an AssumeRole policy, the action to use.
*/
public assumeRoleAction: string = 'sts:AssumeRole';

/**
* Return the policy fragment that identifies this principal in a Policy.
*/
public abstract policyFragment(): PrincipalPolicyFragment;
}

/**
* A collection of the fields in a PolicyStatement that can be used to identify a principal.
*
* This consists of the JSON used in the "Principal" field, and optionally a
* set of "Condition"s that need to be applied to the policy.
*/
export class PrincipalPolicyFragment {
constructor(
public readonly principalJson: { [key: string]: string[] },
public readonly conditions: { [key: string]: any } = { }) {
}
}

export class ArnPrincipal extends PolicyPrincipal {
constructor(public readonly arn: string) {
super();
}

public policyFragment(): PrincipalPolicyFragment {
return new PrincipalPolicyFragment({ AWS: [ this.arn ] });
}
}

export class AccountPrincipal extends ArnPrincipal {
constructor(public readonly accountId: any) {
super(new StackDependentToken(stack => `arn:${stack.partition}:iam::${accountId}:root`).toString());
}
}

/**
* An IAM principal that represents an AWS service (i.e. sqs.amazonaws.com).
*/
export class ServicePrincipal extends PolicyPrincipal {
constructor(public readonly service: string) {
super();
}

public policyFragment(): PrincipalPolicyFragment {
return new PrincipalPolicyFragment({ Service: [ this.service ] });
}
}

/**
* A policy prinicipal for canonicalUserIds - useful for S3 bucket policies that use
* Origin Access identities.
*
* See https://docs.aws.amazon.com/general/latest/gr/acct-identifiers.html
*
* and
*
* https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-restricting-access-to-s3.html
*
* for more details.
*
*/
export class CanonicalUserPrincipal extends PolicyPrincipal {
constructor(public readonly canonicalUserId: string) {
super();
}

public policyFragment(): PrincipalPolicyFragment {
return new PrincipalPolicyFragment({ CanonicalUser: [ this.canonicalUserId ] });
}
}

export class FederatedPrincipal extends PolicyPrincipal {
constructor(
public readonly federated: string,
public readonly conditions: {[key: string]: any},
public assumeRoleAction: string = 'sts:AssumeRole') {
super();
}

public policyFragment(): PrincipalPolicyFragment {
return new PrincipalPolicyFragment({ Federated: [ this.federated ] }, this.conditions);
}
}

export class AccountRootPrincipal extends AccountPrincipal {
constructor() {
super(new StackDependentToken(stack => stack.accountId).toString());
}
}

/**
* A principal representing all identities in all accounts
*/
export class AnyPrincipal extends ArnPrincipal {
constructor() {
super('*');
}
}

/**
* A principal representing all identities in all accounts
* @deprecated use `AnyPrincipal`
*/
export class Anyone extends AnyPrincipal { }

export class CompositePrincipal extends PolicyPrincipal {
private readonly principals = new Array<PolicyPrincipal>();

constructor(principal: PolicyPrincipal, ...additionalPrincipals: PolicyPrincipal[]) {
super();
this.assumeRoleAction = principal.assumeRoleAction;
this.addPrincipals(principal);
this.addPrincipals(...additionalPrincipals);
}

public addPrincipals(...principals: PolicyPrincipal[]): this {
for (const p of principals) {
if (p.assumeRoleAction !== this.assumeRoleAction) {
throw new Error(
`Cannot add multiple principals with different "assumeRoleAction". ` +
`Expecting "${this.assumeRoleAction}", got "${p.assumeRoleAction}"`);
}

const fragment = p.policyFragment();
if (fragment.conditions && Object.keys(fragment.conditions).length > 0) {
throw new Error(
`Components of a CompositePrincipal must not have conditions. ` +
`Tried to add the following fragment: ${JSON.stringify(fragment)}`);
}

this.principals.push(p);
}

return this;
}

public policyFragment(): PrincipalPolicyFragment {
const principalJson: { [key: string]: string[] } = { };

for (const p of this.principals) {
mergePrincipal(principalJson, p.policyFragment().principalJson);
}

return new PrincipalPolicyFragment(principalJson);
}
}

/**
* Represents a statement in an IAM policy document.
*/
Expand Down Expand Up @@ -239,8 +86,8 @@ export class PolicyStatement extends cdk.Token {
return Object.keys(this.principal).length > 0;
}

public addPrincipal(principal: PolicyPrincipal): this {
const fragment = principal.policyFragment();
public addPrincipal(principal: IPrincipal): this {
const fragment = principal.policyFragment;
mergePrincipal(this.principal, fragment.principalJson);
this.addConditions(fragment.conditions);
return this;
Expand Down Expand Up @@ -434,32 +281,3 @@ export enum PolicyStatementEffect {
Allow = 'Allow',
Deny = 'Deny',
}

function mergePrincipal(target: { [key: string]: string[] }, source: { [key: string]: string[] }) {
for (const key of Object.keys(source)) {
target[key] = target[key] || [];

const value = source[key];
if (!Array.isArray(value)) {
throw new Error(`Principal value must be an array (it will be normalized later): ${value}`);
}

target[key].push(...value);
}

return target;
}

/**
* A lazy token that requires an instance of Stack to evaluate
*/
class StackDependentToken extends cdk.Token {
constructor(private readonly fn: (stack: cdk.Stack) => any) {
super();
}

public resolve(context: cdk.ResolveContext) {
const stack = cdk.Stack.find(context.scope);
return this.fn(stack);
}
}
37 changes: 1 addition & 36 deletions packages/@aws-cdk/aws-iam/lib/policy.ts
Original file line number Diff line number Diff line change
@@ -1,46 +1,11 @@
import { Construct, IDependable, Token } from '@aws-cdk/cdk';
import { Group } from './group';
import { CfnPolicy } from './iam.generated';
import { PolicyDocument, PolicyPrincipal, PolicyStatement } from './policy-document';
import { PolicyDocument, PolicyStatement } from './policy-document';
import { IRole } from './role';
import { User } from './user';
import { generatePolicyName, undefinedIfEmpty } from './util';

/**
* A construct that represents an IAM principal, such as a user, group or role.
*/
export interface IPrincipal {
/**
* The IAM principal of this identity (i.e. AWS principal, service principal, etc).
*/
readonly principal: PolicyPrincipal;

/**
* 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;

/**
* Attaches an inline policy to this principal.
* This is the same as calling `policy.addToXxx(principal)`.
* @param policy The policy resource to attach to this principal.
*/
attachInlinePolicy(policy: Policy): void;

/**
* Attaches a managed policy to this principal.
* @param arn The ARN of the managed policy
*/
attachManagedPolicy(arn: string): void;
}

/**
* @deprecated Use IPrincipal
*/
// tslint:disable-next-line:no-empty-interface
export type IIdentityResource = IPrincipal;

export interface PolicyProps {
/**
* The name of the policy. If you specify multiple policies for an entity,
Expand Down
Loading

0 comments on commit 19ad316

Please sign in to comment.