Skip to content

Commit 8259756

Browse files
authored
fix(iam): correctly limit the default PolicyName to 128 characters (#3487)
Our logic for trimming the length of the default IAM policy name was not working, as it wasn't updated when logicalId became a Token rather than a literate string, and so it was never actually triggered (it just checked that the display name of the Token was less than 128 characters, which it always is). The fix is to resolve the logical ID Token before applying the trimming logic. Fixes #3402
1 parent 6733e54 commit 8259756

File tree

3 files changed

+59
-16
lines changed

3 files changed

+59
-16
lines changed

packages/@aws-cdk/aws-iam/lib/policy.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ export class Policy extends Resource implements IPolicy {
9696
// generatePolicyName will take the last 128 characters of the logical id since
9797
// policy names are limited to 128. the last 8 chars are a stack-unique hash, so
9898
// that shouod be sufficient to ensure uniqueness within a principal.
99-
Lazy.stringValue({ produce: () => generatePolicyName(resource.logicalId) })
99+
Lazy.stringValue({ produce: () => generatePolicyName(scope, resource.logicalId) })
100100
});
101101

102102
const resource = new CfnPolicy(this, 'Resource', {

packages/@aws-cdk/aws-iam/lib/util.ts

+21-4
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Lazy } from '@aws-cdk/core';
1+
import { DefaultTokenResolver, IConstruct, Lazy, StringConcat, Tokenization } from '@aws-cdk/core';
22
import { IPolicy } from './policy';
33

44
const MAX_POLICY_NAME_LEN = 128;
@@ -16,8 +16,25 @@ export function undefinedIfEmpty(f: () => string[]): string[] {
1616
* 128 characters, so we take the last 128 characters (in order to make sure the hash
1717
* is there).
1818
*/
19-
export function generatePolicyName(logicalId: string) {
20-
return logicalId.substring(Math.max(logicalId.length - MAX_POLICY_NAME_LEN, 0), logicalId.length);
19+
export function generatePolicyName(scope: IConstruct, logicalId: string): string {
20+
// as logicalId is itself a Token, resolve it first
21+
const resolvedLogicalId = Tokenization.resolve(logicalId, {
22+
scope,
23+
resolver: new DefaultTokenResolver(new StringConcat()),
24+
});
25+
return lastNCharacters(resolvedLogicalId, MAX_POLICY_NAME_LEN);
26+
}
27+
28+
/**
29+
* Returns a string composed of the last n characters of str.
30+
* If str is shorter than n, returns str.
31+
*
32+
* @param str the string to return the last n characters of
33+
* @param n how many characters to return
34+
*/
35+
function lastNCharacters(str: string, n: number) {
36+
const startIndex = Math.max(str.length - n, 0);
37+
return str.substring(startIndex, str.length);
2138
}
2239

2340
/**
@@ -61,4 +78,4 @@ export function mergePrincipal(target: { [key: string]: string[] }, source: { [k
6178
}
6279

6380
return target;
64-
}
81+
}

packages/@aws-cdk/aws-iam/test/test.policy.ts

+37-11
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
import { expect } from '@aws-cdk/assert';
1+
import { expect, haveResourceLike } from '@aws-cdk/assert';
22
import { App, Stack } from '@aws-cdk/core';
33
import { Test } from 'nodeunit';
4-
import { Group, Policy, PolicyStatement, Role, ServicePrincipal, User } from '../lib';
5-
import { generatePolicyName } from '../lib/util';
4+
import { AnyPrincipal, CfnPolicy, Group, Policy, PolicyStatement, Role, ServicePrincipal, User } from '../lib';
5+
6+
// tslint:disable:object-literal-key-quotes
67

78
export = {
89
'fails when policy is empty'(test: Test) {
@@ -254,17 +255,29 @@ export = {
254255
test.done();
255256
},
256257

258+
"generated policy name is the same as the logical id if it's shorter than 128 characters"(test: Test) {
259+
const stack = new Stack();
260+
261+
createPolicyWithLogicalId(stack, 'Foo');
262+
263+
expect(stack).to(haveResourceLike('AWS::IAM::Policy', {
264+
"PolicyName": "Foo",
265+
}));
266+
267+
test.done();
268+
},
269+
257270
'generated policy name only uses the last 128 characters of the logical id'(test: Test) {
258-
test.equal(generatePolicyName('Foo'), 'Foo');
271+
const stack = new Stack();
259272

260-
const logicalId50 = '[' + dup(50 - 2) + ']';
261-
test.equal(generatePolicyName(logicalId50), logicalId50);
273+
const logicalId128 = 'a' + dup(128 - 2) + 'a';
274+
const logicalIdOver128 = 'PREFIX' + logicalId128;
262275

263-
const logicalId128 = '[' + dup(128 - 2) + ']';
264-
test.equal(generatePolicyName(logicalId128), logicalId128);
276+
createPolicyWithLogicalId(stack, logicalIdOver128);
265277

266-
const withPrefix = 'PREFIX' + logicalId128;
267-
test.equal(generatePolicyName(withPrefix), logicalId128, 'ensure prefix is omitted');
278+
expect(stack).to(haveResourceLike('AWS::IAM::Policy', {
279+
"PolicyName": logicalId128,
280+
}));
268281

269282
test.done();
270283

@@ -275,5 +288,18 @@ export = {
275288
}
276289
return r;
277290
}
278-
}
291+
},
279292
};
293+
294+
function createPolicyWithLogicalId(stack: Stack, logicalId: string): void {
295+
const policy = new Policy(stack, logicalId);
296+
const cfnPolicy = policy.node.defaultChild as CfnPolicy;
297+
cfnPolicy.overrideLogicalId(logicalId); // force a particular logical ID
298+
299+
// add statements & principal to satisfy validation
300+
policy.addStatements(new PolicyStatement({
301+
actions: ['*'],
302+
resources: ['*'],
303+
}));
304+
policy.attachToRole(new Role(stack, 'Role', { assumedBy: new AnyPrincipal() }));
305+
}

0 commit comments

Comments
 (0)