Skip to content

Commit daf4e47

Browse files
authored
fix(cfn-diff): handle Fn::If inside policies and statements (#12975)
cloudformation-diff assumes the policies and statements it encounters are simple JSON objects. However, in reality, everywhere that simple object can be used, the Fn::If function can be used as well. Add code that handles that eventuality. Fixes #12887 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent b92188d commit daf4e47

File tree

5 files changed

+226
-69
lines changed

5 files changed

+226
-69
lines changed

packages/@aws-cdk/cloudformation-diff/lib/diffable.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,13 +44,13 @@ interface Eq<T> {
4444
/**
4545
* Whether a collection contains some element (by value)
4646
*/
47-
function contains<T extends Eq<T>>(element: T, xs: T[]) {
47+
function contains<T extends Eq<T>>(element: T, xs: T[]): boolean {
4848
return xs.some(x => x.equal(element));
4949
}
5050

5151
/**
5252
* Return collection except for elements
5353
*/
54-
function difference<T extends Eq<T>>(collection: T[], elements: T[]) {
54+
function difference<T extends Eq<T>>(collection: T[], elements: T[]): T[] {
5555
return collection.filter(x => !contains(x, elements));
5656
}

packages/@aws-cdk/cloudformation-diff/lib/iam/iam-changes.ts

Lines changed: 30 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ import { PropertyChange, PropertyMap, ResourceChange } from '../diff/types';
44
import { DiffableCollection } from '../diffable';
55
import { renderIntrinsics } from '../render-intrinsics';
66
import { deepRemoveUndefined, dropIfEmpty, flatMap, makeComparator } from '../util';
7-
import { ManagedPolicyAttachment, ManagedPolicyJson, parseManagedPolicies } from './managed-policy';
8-
import { parseLambdaPermission, parseStatements, renderCondition, Statement, StatementJson, Targets } from './statement';
7+
import { ManagedPolicyAttachment, ManagedPolicyJson } from './managed-policy';
8+
import { parseLambdaPermission, parseStatements, Statement, StatementJson } from './statement';
99

1010
export interface IamChangesProps {
1111
propertyChanges: PropertyChange[];
@@ -69,23 +69,25 @@ export class IamChanges {
6969

7070
// First generate all lines, then sort on Resource so that similar resources are together
7171
for (const statement of this.statements.additions) {
72+
const renderedStatement = statement.render();
7273
ret.push([
7374
'+',
74-
renderTargets(statement.resources),
75-
statement.effect,
76-
renderTargets(statement.actions),
77-
renderTargets(statement.principals),
78-
renderCondition(statement.condition),
75+
renderedStatement.resource,
76+
renderedStatement.effect,
77+
renderedStatement.action,
78+
renderedStatement.principal,
79+
renderedStatement.condition,
7980
].map(s => colors.green(s)));
8081
}
8182
for (const statement of this.statements.removals) {
83+
const renderedStatement = statement.render();
8284
ret.push([
8385
colors.red('-'),
84-
renderTargets(statement.resources),
85-
statement.effect,
86-
renderTargets(statement.actions),
87-
renderTargets(statement.principals),
88-
renderCondition(statement.condition),
86+
renderedStatement.resource,
87+
renderedStatement.effect,
88+
renderedStatement.action,
89+
renderedStatement.principal,
90+
renderedStatement.condition,
8991
].map(s => colors.red(s)));
9092
}
9193

@@ -125,14 +127,17 @@ export class IamChanges {
125127
}
126128

127129
/**
128-
* Return a machine-readable version of the changes
130+
* Return a machine-readable version of the changes.
131+
* This is only used in tests.
132+
*
133+
* @internal
129134
*/
130-
public toJson(): IamChangesJson {
135+
public _toJson(): IamChangesJson {
131136
return deepRemoveUndefined({
132-
statementAdditions: dropIfEmpty(this.statements.additions.map(s => s.toJson())),
133-
statementRemovals: dropIfEmpty(this.statements.removals.map(s => s.toJson())),
134-
managedPolicyAdditions: dropIfEmpty(this.managedPolicies.additions.map(s => s.toJson())),
135-
managedPolicyRemovals: dropIfEmpty(this.managedPolicies.removals.map(s => s.toJson())),
137+
statementAdditions: dropIfEmpty(this.statements.additions.map(s => s._toJson())),
138+
statementRemovals: dropIfEmpty(this.statements.removals.map(s => s._toJson())),
139+
managedPolicyAdditions: dropIfEmpty(this.managedPolicies.additions.map(s => s._toJson())),
140+
managedPolicyRemovals: dropIfEmpty(this.managedPolicies.removals.map(s => s._toJson())),
136141
});
137142
}
138143

@@ -184,7 +189,11 @@ export class IamChanges {
184189
const appliesToPrincipal = 'AWS:${' + logicalId + '}';
185190

186191
return flatMap(policies, (policy: any) => {
187-
return defaultPrincipal(appliesToPrincipal, parseStatements(renderIntrinsics(policy.PolicyDocument.Statement)));
192+
// check if the Policy itself is not an intrinsic, like an Fn::If
193+
const unparsedStatement = policy.PolicyDocument?.Statement
194+
? policy.PolicyDocument.Statement
195+
: policy;
196+
return defaultPrincipal(appliesToPrincipal, parseStatements(renderIntrinsics(unparsedStatement)));
188197
});
189198
}
190199

@@ -234,11 +243,11 @@ export class IamChanges {
234243
});
235244
}
236245

237-
private readManagedPolicies(policyArns: string[] | undefined, logicalId: string): ManagedPolicyAttachment[] {
246+
private readManagedPolicies(policyArns: any, logicalId: string): ManagedPolicyAttachment[] {
238247
if (!policyArns) { return []; }
239248

240249
const rep = '${' + logicalId + '}';
241-
return parseManagedPolicies(rep, renderIntrinsics(policyArns));
250+
return ManagedPolicyAttachment.parseManagedPolicies(rep, renderIntrinsics(policyArns));
242251
}
243252

244253
private readLambdaStatements(properties?: PropertyMap): Statement[] {
@@ -266,16 +275,6 @@ function defaultResource(resource: string, statements: Statement[]) {
266275
return statements;
267276
}
268277

269-
/**
270-
* Render into a summary table cell
271-
*/
272-
function renderTargets(targets: Targets): string {
273-
if (targets.not) {
274-
return targets.values.map(s => `NOT ${s}`).join('\n');
275-
}
276-
return targets.values.join('\n');
277-
}
278-
279278
export interface IamChangesJson {
280279
statementAdditions?: StatementJson[];
281280
statementRemovals?: StatementJson[];

packages/@aws-cdk/cloudformation-diff/lib/iam/managed-policy.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,25 @@
11
export class ManagedPolicyAttachment {
2+
public static parseManagedPolicies(identityArn: string, arns: string | string[]): ManagedPolicyAttachment[] {
3+
return typeof arns === 'string'
4+
? [new ManagedPolicyAttachment(identityArn, arns)]
5+
: arns.map((arn: string) => new ManagedPolicyAttachment(identityArn, arn));
6+
}
7+
28
constructor(public readonly identityArn: string, public readonly managedPolicyArn: string) {
39
}
410

5-
public equal(other: ManagedPolicyAttachment) {
11+
public equal(other: ManagedPolicyAttachment): boolean {
612
return this.identityArn === other.identityArn
713
&& this.managedPolicyArn === other.managedPolicyArn;
814
}
915

10-
public toJson() {
16+
/**
17+
* Return a machine-readable version of the changes.
18+
* This is only used in tests.
19+
*
20+
* @internal
21+
*/
22+
public _toJson(): ManagedPolicyJson {
1123
return { identityArn: this.identityArn, managedPolicyArn: this.managedPolicyArn };
1224
}
1325
}
@@ -16,7 +28,3 @@ export interface ManagedPolicyJson {
1628
identityArn: string;
1729
managedPolicyArn: string;
1830
}
19-
20-
export function parseManagedPolicies(identityArn: string, arns: string[]): ManagedPolicyAttachment[] {
21-
return arns.map((arn: string) => new ManagedPolicyAttachment(identityArn, arn));
22-
}

packages/@aws-cdk/cloudformation-diff/lib/iam/statement.ts

Lines changed: 82 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -32,36 +32,76 @@ export class Statement {
3232
*/
3333
public readonly condition?: any;
3434

35-
constructor(statement: UnknownMap) {
36-
this.sid = expectString(statement.Sid);
37-
this.effect = expectEffect(statement.Effect);
38-
this.resources = new Targets(statement, 'Resource', 'NotResource');
39-
this.actions = new Targets(statement, 'Action', 'NotAction');
40-
this.principals = new Targets(statement, 'Principal', 'NotPrincipal');
41-
this.condition = statement.Condition;
35+
private readonly serializedIntrinsic: string | undefined;
36+
37+
constructor(statement: UnknownMap | string) {
38+
if (typeof statement === 'string') {
39+
this.sid = undefined;
40+
this.effect = Effect.Unknown;
41+
this.resources = new Targets({}, '', '');
42+
this.actions = new Targets({}, '', '');
43+
this.principals = new Targets({}, '', '');
44+
this.condition = undefined;
45+
this.serializedIntrinsic = statement;
46+
} else {
47+
this.sid = expectString(statement.Sid);
48+
this.effect = expectEffect(statement.Effect);
49+
this.resources = new Targets(statement, 'Resource', 'NotResource');
50+
this.actions = new Targets(statement, 'Action', 'NotAction');
51+
this.principals = new Targets(statement, 'Principal', 'NotPrincipal');
52+
this.condition = statement.Condition;
53+
this.serializedIntrinsic = undefined;
54+
}
4255
}
4356

4457
/**
4558
* Whether this statement is equal to the other statement
4659
*/
47-
public equal(other: Statement) {
60+
public equal(other: Statement): boolean {
4861
return (this.sid === other.sid
4962
&& this.effect === other.effect
63+
&& this.serializedIntrinsic === other.serializedIntrinsic
5064
&& this.resources.equal(other.resources)
5165
&& this.actions.equal(other.actions)
5266
&& this.principals.equal(other.principals)
5367
&& deepEqual(this.condition, other.condition));
5468
}
5569

56-
public toJson(): StatementJson {
57-
return deepRemoveUndefined({
58-
sid: this.sid,
59-
effect: this.effect,
60-
resources: this.resources.toJson(),
61-
principals: this.principals.toJson(),
62-
actions: this.actions.toJson(),
63-
condition: this.condition,
64-
});
70+
public render(): RenderedStatement {
71+
return this.serializedIntrinsic
72+
? {
73+
resource: this.serializedIntrinsic,
74+
effect: '',
75+
action: '',
76+
principal: this.principals.render(), // these will be replaced by the call to replaceEmpty() from IamChanges
77+
condition: '',
78+
}
79+
: {
80+
resource: this.resources.render(),
81+
effect: this.effect,
82+
action: this.actions.render(),
83+
principal: this.principals.render(),
84+
condition: renderCondition(this.condition),
85+
};
86+
}
87+
88+
/**
89+
* Return a machine-readable version of the changes.
90+
* This is only used in tests.
91+
*
92+
* @internal
93+
*/
94+
public _toJson(): StatementJson {
95+
return this.serializedIntrinsic
96+
? this.serializedIntrinsic
97+
: deepRemoveUndefined({
98+
sid: this.sid,
99+
effect: this.effect,
100+
resources: this.resources._toJson(),
101+
principals: this.principals._toJson(),
102+
actions: this.actions._toJson(),
103+
condition: this.condition,
104+
});
65105
}
66106

67107
/**
@@ -76,6 +116,14 @@ export class Statement {
76116
}
77117
}
78118

119+
export interface RenderedStatement {
120+
readonly resource: string;
121+
readonly effect: string;
122+
readonly action: string;
123+
readonly principal: string;
124+
readonly condition: string;
125+
}
126+
79127
export interface StatementJson {
80128
sid?: string;
81129
effect: string;
@@ -199,7 +247,22 @@ export class Targets {
199247
this.values.sort();
200248
}
201249

202-
public toJson(): TargetsJson {
250+
/**
251+
* Render into a summary table cell
252+
*/
253+
public render(): string {
254+
return this.not
255+
? this.values.map(s => `NOT ${s}`).join('\n')
256+
: this.values.join('\n');
257+
}
258+
259+
/**
260+
* Return a machine-readable version of the changes.
261+
* This is only used in tests.
262+
*
263+
* @internal
264+
*/
265+
public _toJson(): TargetsJson {
203266
return { not: this.not, values: this.values };
204267
}
205268
}
@@ -243,7 +306,7 @@ function forceListOfStrings(x: unknown): string[] {
243306
/**
244307
* Render the Condition column
245308
*/
246-
export function renderCondition(condition: any) {
309+
export function renderCondition(condition: any): string {
247310
if (!condition || Object.keys(condition).length === 0) { return ''; }
248311
const jsonRepresentation = JSON.stringify(condition, undefined, 2);
249312

0 commit comments

Comments
 (0)