Skip to content

Commit 3fc5c8c

Browse files
authored
fix(iam): Merge multiple principals correctly (aws#983)
When adidng multiple principals, an array of principals was created, but IAM documents expect an object with different keys, for which values may be arrays. This corrects the merging process so it produces valid statements. This restores the fix for aws#924 that was introduced in aws#916 and reverted in aws#958 due to an other feature part of the same commit.
1 parent b42e742 commit 3fc5c8c

File tree

2 files changed

+87
-19
lines changed

2 files changed

+87
-19
lines changed

Diff for: packages/@aws-cdk/aws-iam/lib/policy-document.ts

+35-17
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ export abstract class PolicyPrincipal {
6565
*/
6666
export class PrincipalPolicyFragment {
6767
constructor(
68-
public readonly principalJson: any,
68+
public readonly principalJson: { [key: string]: any },
6969
public readonly conditions: {[key: string]: any} = {}) {
7070
}
7171
}
@@ -144,18 +144,9 @@ export class AccountRootPrincipal extends AccountPrincipal {
144144
/**
145145
* A principal representing all identities in all accounts
146146
*/
147-
export class Anyone extends PolicyPrincipal {
148-
/**
149-
* Interface compatibility with AccountPrincipal for the purposes of the Lambda library
150-
*
151-
* The Lambda's addPermission() call works differently from regular
152-
* statements, and will use the value of this property directly if present
153-
* (which leads to the correct statement ultimately).
154-
*/
155-
public readonly accountId = '*';
156-
157-
public policyFragment(): PrincipalPolicyFragment {
158-
return new PrincipalPolicyFragment('*');
147+
export class Anyone extends ArnPrincipal {
148+
constructor() {
149+
super('*');
159150
}
160151
}
161152

@@ -164,7 +155,7 @@ export class Anyone extends PolicyPrincipal {
164155
*/
165156
export class PolicyStatement extends Token {
166157
private action = new Array<any>();
167-
private principal = new Array<any>();
158+
private principal: { [key: string]: any[] } = {};
168159
private resource = new Array<any>();
169160
private condition: { [key: string]: any } = { };
170161
private effect?: PolicyStatementEffect;
@@ -197,12 +188,23 @@ export class PolicyStatement extends Token {
197188
* Indicates if this permission has a "Principal" section.
198189
*/
199190
public get hasPrincipal() {
200-
return this.principal && this.principal.length > 0;
191+
return Object.keys(this.principal).length > 0;
201192
}
202193

203194
public addPrincipal(principal: PolicyPrincipal): PolicyStatement {
204195
const fragment = principal.policyFragment();
205-
this.principal.push(fragment.principalJson);
196+
for (const key of Object.keys(fragment.principalJson)) {
197+
if (Object.keys(this.principal).length > 0 && !(key in this.principal)) {
198+
throw new Error(`Attempted to add principal key ${key} in principal of type ${Object.keys(this.principal)[0]}`);
199+
}
200+
this.principal[key] = this.principal[key] || [];
201+
const value = fragment.principalJson[key];
202+
if (Array.isArray(value)) {
203+
this.principal[key].push(...value);
204+
} else {
205+
this.principal[key].push(value);
206+
}
207+
}
206208
this.addConditions(fragment.conditions);
207209
return this;
208210
}
@@ -330,7 +332,7 @@ export class PolicyStatement extends Token {
330332
Action: _norm(this.action),
331333
Condition: _norm(this.condition),
332334
Effect: _norm(this.effect),
333-
Principal: _norm(this.principal),
335+
Principal: _normPrincipal(this.principal),
334336
Resource: _norm(this.resource),
335337
Sid: _norm(this.sid),
336338
};
@@ -361,6 +363,22 @@ export class PolicyStatement extends Token {
361363

362364
return values;
363365
}
366+
367+
function _normPrincipal(principal: { [key: string]: any[] }) {
368+
const keys = Object.keys(principal);
369+
if (keys.length === 0) { return undefined; }
370+
const result: any = {};
371+
for (const key of keys) {
372+
const normVal = _norm(principal[key]);
373+
if (normVal) {
374+
result[key] = normVal;
375+
}
376+
}
377+
if (Object.keys(result).length === 1 && result.AWS === '*') {
378+
return '*';
379+
}
380+
return result;
381+
}
364382
}
365383
}
366384

Diff for: packages/@aws-cdk/aws-iam/test/test.policy-document.ts

+52-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { FnConcat, resolve } from '@aws-cdk/cdk';
22
import { Test } from 'nodeunit';
3-
import { CanonicalUserPrincipal, PolicyDocument, PolicyStatement } from '../lib';
3+
import { Anyone, CanonicalUserPrincipal, PolicyDocument, PolicyPrincipal, PolicyStatement, PrincipalPolicyFragment } from '../lib';
44

55
export = {
66
'the Permission class is a programming model for iam'(test: Test) {
@@ -143,6 +143,22 @@ export = {
143143
test.done();
144144
},
145145

146+
'addAwsAccountPrincipal can be used multiple times'(test: Test) {
147+
const p = new PolicyStatement();
148+
p.addAwsAccountPrincipal('1234');
149+
p.addAwsAccountPrincipal('5678');
150+
test.deepEqual(resolve(p), {
151+
Effect: 'Allow',
152+
Principal: {
153+
AWS: [
154+
{ 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':iam::1234:root']] },
155+
{ 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':iam::5678:root']] }
156+
]
157+
}
158+
});
159+
test.done();
160+
},
161+
146162
'hasResource': {
147163
'false if there are no resources'(test: Test) {
148164
test.equal(new PolicyStatement().hasResource, false, 'hasResource should be false for an empty permission');
@@ -188,5 +204,39 @@ export = {
188204
p.addStatement(new PolicyStatement());
189205
test.equal(p.statementCount, 2);
190206
test.done();
191-
}
207+
},
208+
209+
'the { AWS: "*" } principal is represented as "*"'(test: Test) {
210+
const p = new PolicyDocument().addStatement(new PolicyStatement().addPrincipal(new Anyone()));
211+
test.deepEqual(resolve(p), { Statement: [{ Effect: 'Allow', Principal: '*' }], Version: '2012-10-17' });
212+
test.done();
213+
},
214+
215+
'addPrincipal prohibits mixing principal types'(test: Test) {
216+
const s = new PolicyStatement().addAccountRootPrincipal();
217+
test.throws(() => { s.addServicePrincipal('rds.amazonaws.com'); },
218+
/Attempted to add principal key Service/);
219+
test.throws(() => { s.addFederatedPrincipal('federation', { ConditionOp: { ConditionKey: 'ConditionValue' } }); },
220+
/Attempted to add principal key Federated/);
221+
test.done();
222+
},
223+
224+
'addPrincipal correctly merges array in'(test: Test) {
225+
const arrayPrincipal: PolicyPrincipal = {
226+
assumeRoleAction: 'sts:AssumeRole',
227+
policyFragment: () => new PrincipalPolicyFragment({ AWS: ['foo', 'bar'] }),
228+
};
229+
const s = new PolicyStatement().addAccountRootPrincipal()
230+
.addPrincipal(arrayPrincipal);
231+
test.deepEqual(resolve(s), {
232+
Effect: 'Allow',
233+
Principal: {
234+
AWS: [
235+
{ 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':iam::', { Ref: 'AWS::AccountId' }, ':root']] },
236+
'foo', 'bar'
237+
]
238+
}
239+
});
240+
test.done();
241+
},
192242
};

0 commit comments

Comments
 (0)