diff --git a/packages/@aws-cdk/aws-iam/README.md b/packages/@aws-cdk/aws-iam/README.md index 30ec76bd767dc..d03c162d306ed 100644 --- a/packages/@aws-cdk/aws-iam/README.md +++ b/packages/@aws-cdk/aws-iam/README.md @@ -71,7 +71,7 @@ If multiple principals are added to the policy statement, they will be merged to const statement = new PolicyStatement(); statement.addServicePrincipal('cloudwatch.amazonaws.com'); statement.addServicePrincipal('ec2.amazonaws.com'); -statement.addAwsPrincipal('arn:aws:boom:boom'); +statement.addArnPrincipal('arn:aws:boom:boom'); ``` Will result in: diff --git a/packages/@aws-cdk/aws-iam/lib/policy-document.ts b/packages/@aws-cdk/aws-iam/lib/policy-document.ts index 94e9a81167941..fd0bafe92cc80 100644 --- a/packages/@aws-cdk/aws-iam/lib/policy-document.ts +++ b/packages/@aws-cdk/aws-iam/lib/policy-document.ts @@ -1,6 +1,6 @@ import cdk = require('@aws-cdk/cdk'); import { IPostProcessor } from '@aws-cdk/cdk'; -import { PolicyStatement } from './policy-statement'; +import { IPolicyStatement } from './policy-statement'; /** * Properties for a new PolicyDocument @@ -18,7 +18,7 @@ export interface PolicyDocumentProps { * A PolicyDocument is a collection of statements */ export class PolicyDocument implements cdk.IResolvable { - private readonly statements = new Array(); + private readonly statements = new Array(); private readonly autoAssignSids: boolean; constructor(props: PolicyDocumentProps = {}) { @@ -47,7 +47,7 @@ export class PolicyDocument implements cdk.IResolvable { * * @param statement the statement to add. */ - public addStatements(...statement: PolicyStatement[]) { + public addStatements(...statement: IPolicyStatement[]) { this.statements.push(...statement); } @@ -60,13 +60,22 @@ export class PolicyDocument implements cdk.IResolvable { }); } + /** + * JSON-ify the document + * + * Used when JSON.stringify() is called + */ + public toJSON() { + return this.render(); + } + private render(): any { if (this.isEmpty) { return undefined; } const doc = { - Statement: this.statements, + Statement: this.statements.map(s => s.toStatementJson()), Version: '2012-10-17' }; diff --git a/packages/@aws-cdk/aws-iam/lib/policy-statement.ts b/packages/@aws-cdk/aws-iam/lib/policy-statement.ts index 2c1bf42d5ec3e..ec5bb844a80c8 100644 --- a/packages/@aws-cdk/aws-iam/lib/policy-statement.ts +++ b/packages/@aws-cdk/aws-iam/lib/policy-statement.ts @@ -3,10 +3,34 @@ import { AccountPrincipal, AccountRootPrincipal, Anyone, ArnPrincipal, Canonical FederatedPrincipal, IPrincipal, ServicePrincipal, ServicePrincipalOpts } from './principals'; import { mergePrincipal } from './util'; +/** + * An abstract PolicyStatement + */ +export interface IPolicyStatement { + /** + * Render the policy statement to JSON + */ + toStatementJson(): any; +} + /** * Represents a statement in an IAM policy document. */ -export class PolicyStatement implements cdk.IResolvable { +export class PolicyStatement implements IPolicyStatement { + public static fromAttributes(attrs: PolicyStatementAttributes): IPolicyStatement { + const st = new PolicyStatement(); + st.addActions(...attrs.actions || []); + (attrs.principals || []).forEach(p => st.addPrincipal(p)); + st.addResources(...attrs.resourceArns || []); + if (attrs.conditions !== undefined) { + st.addConditions(attrs.conditions); + } + return st; + } + + /** + * Statement ID for this statement + */ public sid?: string; private action = new Array(); @@ -51,16 +75,12 @@ export class PolicyStatement implements cdk.IResolvable { return this; } - public addAwsPrincipal(arn: string): this { - return this.addPrincipal(new ArnPrincipal(arn)); - } - public addAwsAccountPrincipal(accountId: string): this { return this.addPrincipal(new AccountPrincipal(accountId)); } public addArnPrincipal(arn: string): this { - return this.addAwsPrincipal(arn); + return this.addPrincipal(new ArnPrincipal(arn)); } /** @@ -180,14 +200,7 @@ export class PolicyStatement implements cdk.IResolvable { return this.addCondition('StringEquals', { 'sts:ExternalId': accountId }); } - // - // Serialization - // - public resolve(_context: cdk.IResolveContext): any { - return this.toJson(); - } - - public toJson(): any { + public toStatementJson(): any { return { Action: _norm(this.action), Condition: _norm(this.condition), @@ -251,12 +264,50 @@ export class PolicyStatement implements cdk.IResolvable { }); } + /** + * JSON-ify the statement + * + * Used when JSON.stringify() is called + */ public toJSON() { - return this.toJson(); + return this.toStatementJson(); } } export enum PolicyStatementEffect { Allow = 'Allow', Deny = 'Deny', +} + +/** + * Interface for creating a policy statement + */ +export interface PolicyStatementAttributes { + /** + * List of actions to add to the statement + * + * @default - no actions + */ + actions?: string[]; + + /** + * List of principals to add to the statement + * + * @default - no principals + */ + principals?: IPrincipal[]; + + /** + * Resource ARNs to add to the statement + * + * @default - no principals + */ + resourceArns?: string[]; + + /** + * Conditions to add to the statement + * + * @default - no condition + */ + conditions?: {[key: string]: any}; } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-iam/test/test.policy-document.ts b/packages/@aws-cdk/aws-iam/test/test.policy-document.ts index 6ced727881df4..2c3f60b6f8484 100644 --- a/packages/@aws-cdk/aws-iam/test/test.policy-document.ts +++ b/packages/@aws-cdk/aws-iam/test/test.policy-document.ts @@ -17,7 +17,7 @@ export = { p.addAwsAccountPrincipal(`my${Token.asString({ account: 'account' })}name`); p.limitToAccount('12221121221'); - test.deepEqual(stack.resolve(p), { Action: + test.deepEqual(stack.resolve(p.toStatementJson()), { Action: [ 'sqs:SendMessage', 'dynamodb:CreateTable', 'dynamodb:DeleteTable' ], @@ -63,7 +63,7 @@ export = { 'Permission allows specifying multiple actions upon construction'(test: Test) { const stack = new Stack(); const perm = new PolicyStatement().addResource('MyResource').addActions('Action1', 'Action2', 'Action3'); - test.deepEqual(stack.resolve(perm), { + test.deepEqual(stack.resolve(perm.toStatementJson()), { Effect: 'Allow', Action: [ 'Action1', 'Action2', 'Action3' ], Resource: 'MyResource' }); @@ -82,7 +82,7 @@ export = { const p = new PolicyStatement(); const canoncialUser = "averysuperduperlongstringfor"; p.addPrincipal(new CanonicalUserPrincipal(canoncialUser)); - test.deepEqual(stack.resolve(p), { + test.deepEqual(stack.resolve(p.toStatementJson()), { Effect: "Allow", Principal: { CanonicalUser: canoncialUser @@ -96,7 +96,7 @@ export = { const p = new PolicyStatement(); p.addAccountRootPrincipal(); - test.deepEqual(stack.resolve(p), { + test.deepEqual(stack.resolve(p.toStatementJson()), { Effect: "Allow", Principal: { AWS: { @@ -120,7 +120,7 @@ export = { const stack = new Stack(); const p = new PolicyStatement(); p.addFederatedPrincipal("com.amazon.cognito", { StringEquals: { key: 'value' }}); - test.deepEqual(stack.resolve(p), { + test.deepEqual(stack.resolve(p.toStatementJson()), { Effect: "Allow", Principal: { Federated: "com.amazon.cognito" @@ -138,7 +138,7 @@ export = { const p = new PolicyStatement(); p.addAwsAccountPrincipal('1234'); p.addAwsAccountPrincipal('5678'); - test.deepEqual(stack.resolve(p), { + test.deepEqual(stack.resolve(p.toStatementJson()), { Effect: 'Allow', Principal: { AWS: [ @@ -181,7 +181,7 @@ export = { 'true if there is a principal'(test: Test) { const p = new PolicyStatement(); - p.addAwsPrincipal('bla'); + p.addArnPrincipal('bla'); test.equal(p.hasPrincipal, true); test.done(); } @@ -244,26 +244,6 @@ export = { } }, - 'addAwsPrincipal/addArnPrincipal are the aliases'(test: Test) { - const stack = new Stack(); - const p = new PolicyDocument(); - - p.addStatements(new PolicyStatement().addAwsPrincipal('111222-A')); - p.addStatements(new PolicyStatement().addArnPrincipal('111222-B')); - p.addStatements(new PolicyStatement().addPrincipal(new ArnPrincipal('111222-C'))); - - test.deepEqual(stack.resolve(p), { - Statement: [ { - Effect: 'Allow', Principal: { AWS: '111222-A' } }, - { Effect: 'Allow', Principal: { AWS: '111222-B' } }, - { Effect: 'Allow', Principal: { AWS: '111222-C' } } - ], - Version: '2012-10-17' - }); - - test.done(); - }, - 'addResources() will not break a list-encoded Token'(test: Test) { const stack = new Stack(); @@ -271,7 +251,7 @@ export = { .addActions(...Lazy.listValue({ produce: () => ['a', 'b', 'c'] })) .addResources(...Lazy.listValue({ produce: () => ['x', 'y', 'z'] })); - test.deepEqual(stack.resolve(statement), { + test.deepEqual(stack.resolve(statement.toStatementJson()), { Effect: 'Allow', Action: ['a', 'b', 'c'], Resource: ['x', 'y', 'z'], @@ -308,7 +288,7 @@ export = { }; const s = new PolicyStatement().addAccountRootPrincipal() .addPrincipal(arrayPrincipal); - test.deepEqual(stack.resolve(s), { + test.deepEqual(stack.resolve(s.toStatementJson()), { Effect: 'Allow', Principal: { AWS: [ @@ -324,12 +304,12 @@ export = { 'policy statements with multiple principal types can be created using multiple addPrincipal calls'(test: Test) { const stack = new Stack(); const s = new PolicyStatement() - .addAwsPrincipal('349494949494') + .addArnPrincipal('349494949494') .addServicePrincipal('test.service') .addResource('resource') .addAction('action'); - test.deepEqual(stack.resolve(s), { + test.deepEqual(stack.resolve(s.toStatementJson()), { Action: 'action', Effect: 'Allow', Principal: { AWS: '349494949494', Service: 'test.service' }, @@ -346,7 +326,7 @@ export = { .addAction('test:Action') .addServicePrincipal('codedeploy.amazonaws.com'); - test.deepEqual(stack.resolve(s), { + test.deepEqual(stack.resolve(s.toStatementJson()), { Effect: 'Allow', Action: 'test:Action', Principal: { Service: 'codedeploy.cn-north-1.amazonaws.com.cn' } @@ -361,7 +341,7 @@ export = { .addAction('test:Action') .addServicePrincipal('codedeploy.amazonaws.com', { region: 'cn-north-1' }); - test.deepEqual(stack.resolve(s), { + test.deepEqual(stack.resolve(s.toStatementJson()), { Effect: 'Allow', Action: 'test:Action', Principal: { Service: 'codedeploy.cn-north-1.amazonaws.com.cn' } @@ -376,7 +356,7 @@ export = { .addAction('test:Action') .addServicePrincipal('test.service-principal.dev'); - test.deepEqual(stack.resolve(s), { + test.deepEqual(stack.resolve(s.toStatementJson()), { Effect: 'Allow', Action: 'test:Action', Principal: { Service: 'test.service-principal.dev' } @@ -392,7 +372,7 @@ export = { const stack = new Stack(); const p = new CompositePrincipal(new ArnPrincipal('i:am:an:arn')); const statement = new PolicyStatement().addPrincipal(p); - test.deepEqual(stack.resolve(statement), { Effect: 'Allow', Principal: { AWS: 'i:am:an:arn' } }); + test.deepEqual(stack.resolve(statement.toStatementJson()), { Effect: 'Allow', Principal: { AWS: 'i:am:an:arn' } }); test.done(); }, @@ -420,10 +400,10 @@ export = { const statement = new PolicyStatement().addPrincipal(p); // add via policy statement - statement.addAwsPrincipal('aws-principal-3'); + statement.addArnPrincipal('aws-principal-3'); statement.addCondition('cond2', { boom: 123 }); - test.deepEqual(stack.resolve(statement), { + test.deepEqual(stack.resolve(statement.toStatementJson()), { Condition: { cond2: { boom: 123 } }, @@ -524,5 +504,30 @@ export = { ], }); test.done(); - } -}; + }, + + 'fromAttributes is equivalent'(test: Test) { + const stack = new Stack(); + + const doc1 = new PolicyDocument(); + doc1.addStatements(new PolicyStatement() + .addActions('action1', 'action2') + .addAllResources() + .addArnPrincipal('arn') + .addCondition('key', { equals: 'value' })); + + const doc2 = new PolicyDocument(); + doc2.addStatements(PolicyStatement.fromAttributes({ + actions: ['action1', 'action2'], + resourceArns: ['*'], + principals: [new ArnPrincipal('arn')], + conditions: { + key: { equals: 'value' } + } + })); + + test.deepEqual(stack.resolve(doc1), stack.resolve(doc2)); + + test.done(); + }, +}; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-kms/test/integ.key.ts b/packages/@aws-cdk/aws-kms/test/integ.key.ts index a1dcfd66a757a..c212903dfefa1 100644 --- a/packages/@aws-cdk/aws-kms/test/integ.key.ts +++ b/packages/@aws-cdk/aws-kms/test/integ.key.ts @@ -11,7 +11,7 @@ const key = new Key(stack, 'MyKey', { retain: false }); key.addToResourcePolicy(new PolicyStatement() .addAllResources() .addAction('kms:encrypt') - .addAwsPrincipal(stack.accountId)); + .addArnPrincipal(stack.accountId)); key.addAlias('alias/bar'); diff --git a/packages/@aws-cdk/aws-kms/test/test.key.ts b/packages/@aws-cdk/aws-kms/test/test.key.ts index 4923791b90093..b8cef36985346 100644 --- a/packages/@aws-cdk/aws-kms/test/test.key.ts +++ b/packages/@aws-cdk/aws-kms/test/test.key.ts @@ -80,7 +80,7 @@ export = { const key = new Key(stack, 'MyKey'); const p = new PolicyStatement().addAllResources().addAction('kms:encrypt'); - p.addAwsPrincipal('arn'); + p.addArnPrincipal('arn'); key.addToResourcePolicy(p); expect(stack).to(exactlyMatchTemplate({ @@ -154,7 +154,7 @@ export = { enabled: false, }); const p = new PolicyStatement().addAllResources().addAction('kms:encrypt'); - p.addAwsPrincipal('arn'); + p.addArnPrincipal('arn'); key.addToResourcePolicy(p); key.node.applyAspect(new Tag('tag1', 'value1'));