Skip to content

Commit 86997d8

Browse files
kaizenccmrgrain
andauthored
refactor(toolkit-lib): remove requireApproval option from diff (#372)
`requireApproval` is deprecated and has been moved to the `CliIoHost`. this PR removes the last place where `requireApproval` was necessary in `tmp-toolkit-helpers`, in `formatSecurityDiff`. `formatSecurityDiff` now makes no _decisions_ on what to print; it returns the diff and a `permissionChangeType` for the `IoHost` to interpret. this is a slight behavior change, so all consumers of `formatSecurityDiff have been updated accordingly (`deploy` and `diff` in `toolkit-lib` and in the CLI). `requireApproval` is now a vestigial structure only in use in the CLI. --- By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license --------- Co-authored-by: Momo Kornher <[email protected]>
1 parent 840ad5a commit 86997d8

File tree

7 files changed

+117
-175
lines changed

7 files changed

+117
-175
lines changed

packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-lib-security-related-changes-without-a-cli-are-expected-to-fail-when-approval-is-required.integtest.ts

-3
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,6 @@ integTest(
1313
neverRequireApproval: false,
1414
});
1515

16-
expect(stdErr).toContain(
17-
'This deployment will make potentially sensitive changes according to your current security approval level',
18-
);
1916
expect(stdErr).toContain(
2017
'"--require-approval" is enabled and stack includes security-sensitive updates',
2118
);

packages/@aws-cdk/tmp-toolkit-helpers/src/api/diff/diff-formatter.ts

+65-59
Original file line numberDiff line numberDiff line change
@@ -9,21 +9,26 @@ import {
99
} from '@aws-cdk/cloudformation-diff';
1010
import type * as cxapi from '@aws-cdk/cx-api';
1111
import * as chalk from 'chalk';
12+
import { PermissionChangeType } from '../../payloads';
1213
import type { NestedStackTemplates } from '../cloudformation';
1314
import type { IoHelper } from '../io/private';
1415
import { IoDefaultMessages } from '../io/private';
15-
import { RequireApproval } from '../require-approval';
1616
import { StringWriteStream } from '../streams';
17-
import { ToolkitError } from '../toolkit-error';
1817

1918
/**
2019
* Output of formatSecurityDiff
2120
*/
2221
interface FormatSecurityDiffOutput {
2322
/**
24-
* Complete formatted security diff, if it is prompt-worthy
23+
* Complete formatted security diff
2524
*/
26-
readonly formattedDiff?: string;
25+
readonly formattedDiff: string;
26+
27+
/**
28+
* The type of permission changes in the security diff.
29+
* The IoHost will use this to decide whether or not to print.
30+
*/
31+
readonly permissionChangeType: PermissionChangeType;
2732
}
2833

2934
/**
@@ -57,16 +62,6 @@ interface DiffFormatterProps {
5762
readonly templateInfo: TemplateInfo;
5863
}
5964

60-
/**
61-
* Properties specific to formatting the security diff
62-
*/
63-
interface FormatSecurityDiffOptions {
64-
/**
65-
* The approval level of the security diff
66-
*/
67-
readonly requireApproval: RequireApproval;
68-
}
69-
7065
/**
7166
* PRoperties specific to formatting the stack diff
7267
*/
@@ -169,6 +164,42 @@ export class DiffFormatter {
169164
return this._diffs;
170165
}
171166

167+
/**
168+
* Get or creates the diff of a stack.
169+
* If it creates the diff, it stores the result in a map for
170+
* easier retreval later.
171+
*/
172+
private diff(stackName?: string, oldTemplate?: any) {
173+
const realStackName = stackName ?? this.stackName;
174+
175+
if (!this._diffs[realStackName]) {
176+
this._diffs[realStackName] = fullDiff(
177+
oldTemplate ?? this.oldTemplate,
178+
this.newTemplate.template,
179+
this.changeSet,
180+
this.isImport,
181+
);
182+
}
183+
return this._diffs[realStackName];
184+
}
185+
186+
/**
187+
* Return whether the diff has security-impacting changes that need confirmation.
188+
*
189+
* If no stackName is given, then the root stack name is used.
190+
*/
191+
private permissionType(stackName?: string): PermissionChangeType {
192+
const diff = this.diff(stackName);
193+
194+
if (diff.permissionsBroadened) {
195+
return PermissionChangeType.BROADENING;
196+
} else if (diff.permissionsAnyChanges) {
197+
return PermissionChangeType.NON_BROADENING;
198+
} else {
199+
return PermissionChangeType.NONE;
200+
}
201+
}
202+
172203
/**
173204
* Format the stack diff
174205
*/
@@ -191,8 +222,7 @@ export class DiffFormatter {
191222
nestedStackTemplates: { [nestedStackLogicalId: string]: NestedStackTemplates } | undefined,
192223
options: ReusableStackDiffOptions,
193224
) {
194-
let diff = fullDiff(oldTemplate, this.newTemplate.template, this.changeSet, this.isImport);
195-
this._diffs[stackName] = diff;
225+
let diff = this.diff(stackName, oldTemplate);
196226

197227
// The stack diff is formatted via `Formatter`, which takes in a stream
198228
// and sends its output directly to that stream. To faciliate use of the
@@ -277,51 +307,27 @@ export class DiffFormatter {
277307
/**
278308
* Format the security diff
279309
*/
280-
public formatSecurityDiff(options: FormatSecurityDiffOptions): FormatSecurityDiffOutput {
281-
const ioDefaultHelper = new IoDefaultMessages(this.ioHelper);
310+
public formatSecurityDiff(): FormatSecurityDiffOutput {
311+
const diff = this.diff();
282312

283-
const diff = fullDiff(this.oldTemplate, this.newTemplate.template, this.changeSet);
284-
this._diffs[this.stackName] = diff;
285-
286-
if (diffRequiresApproval(diff, options.requireApproval)) {
287-
// The security diff is formatted via `Formatter`, which takes in a stream
288-
// and sends its output directly to that stream. To faciliate use of the
289-
// global CliIoHost, we create our own stream to capture the output of
290-
// `Formatter` and return the output as a string for the consumer of
291-
// `formatSecurityDiff` to decide what to do with it.
292-
const stream = new StringWriteStream();
293-
294-
stream.write(format(`Stack ${chalk.bold(this.stackName)}\n`));
295-
296-
// eslint-disable-next-line max-len
297-
ioDefaultHelper.warning(`This deployment will make potentially sensitive changes according to your current security approval level (--require-approval ${options.requireApproval}).`);
298-
ioDefaultHelper.warning('Please confirm you intend to make the following modifications:\n');
299-
try {
300-
// formatSecurityChanges updates the stream with the formatted security diff
301-
formatSecurityChanges(stream, diff, buildLogicalToPathMap(this.newTemplate));
302-
} finally {
303-
stream.end();
304-
}
305-
// store the stream containing a formatted stack diff
306-
const formattedDiff = stream.toString();
307-
return { formattedDiff };
308-
}
309-
return {};
310-
}
311-
}
313+
// The security diff is formatted via `Formatter`, which takes in a stream
314+
// and sends its output directly to that stream. To faciliate use of the
315+
// global CliIoHost, we create our own stream to capture the output of
316+
// `Formatter` and return the output as a string for the consumer of
317+
// `formatSecurityDiff` to decide what to do with it.
318+
const stream = new StringWriteStream();
312319

313-
/**
314-
* Return whether the diff has security-impacting changes that need confirmation
315-
*
316-
* TODO: Filter the security impact determination based off of an enum that allows
317-
* us to pick minimum "severities" to alert on.
318-
*/
319-
function diffRequiresApproval(diff: TemplateDiff, requireApproval: RequireApproval) {
320-
switch (requireApproval) {
321-
case RequireApproval.NEVER: return false;
322-
case RequireApproval.ANY_CHANGE: return diff.permissionsAnyChanges;
323-
case RequireApproval.BROADENING: return diff.permissionsBroadened;
324-
default: throw new ToolkitError(`Unrecognized approval level: ${requireApproval}`);
320+
stream.write(format(`Stack ${chalk.bold(this.stackName)}\n`));
321+
322+
try {
323+
// formatSecurityChanges updates the stream with the formatted security diff
324+
formatSecurityChanges(stream, diff, buildLogicalToPathMap(this.newTemplate));
325+
} finally {
326+
stream.end();
327+
}
328+
// store the stream containing a formatted stack diff
329+
const formattedDiff = stream.toString();
330+
return { formattedDiff, permissionChangeType: this.permissionType() };
325331
}
326332
}
327333

packages/@aws-cdk/tmp-toolkit-helpers/test/api/diff/diff.test.ts

+6-66
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import type * as cxapi from '@aws-cdk/cx-api';
22
import * as chalk from 'chalk';
33
import { DiffFormatter } from '../../../src/api/diff/diff-formatter';
44
import { IoHelper, IoDefaultMessages } from '../../../src/api/io/private';
5-
import { RequireApproval } from '../../../src/api/require-approval';
65

76
jest.mock('../../../src/api/io/private/messages', () => ({
87
IoDefaultMessages: jest.fn(),
@@ -214,7 +213,7 @@ describe('formatSecurityDiff', () => {
214213
} as any;
215214
});
216215

217-
test('returns empty object when no security changes exist', () => {
216+
test('returns information on security changes for the IoHost to interpret', () => {
218217
// WHEN
219218
const formatter = new DiffFormatter({
220219
ioHelper: mockIoHelper,
@@ -223,16 +222,14 @@ describe('formatSecurityDiff', () => {
223222
newTemplate: mockNewTemplate,
224223
},
225224
});
226-
const result = formatter.formatSecurityDiff({
227-
requireApproval: RequireApproval.BROADENING,
228-
});
225+
const result = formatter.formatSecurityDiff();
229226

230227
// THEN
231-
expect(result.formattedDiff).toBeUndefined();
228+
expect(result.permissionChangeType).toEqual('none');
232229
expect(mockIoDefaultMessages.warning).not.toHaveBeenCalled();
233230
});
234231

235-
test('formats diff when permissions are broadened and approval level is BROADENING', () => {
232+
test('returns formatted diff for broadening security changes', () => {
236233
// WHEN
237234
const formatter = new DiffFormatter({
238235
ioHelper: mockIoHelper,
@@ -241,12 +238,10 @@ describe('formatSecurityDiff', () => {
241238
newTemplate: mockNewTemplate,
242239
},
243240
});
244-
const result = formatter.formatSecurityDiff({
245-
requireApproval: RequireApproval.BROADENING,
246-
});
241+
const result = formatter.formatSecurityDiff();
247242

248243
// THEN
249-
expect(result.formattedDiff).toBeDefined();
244+
expect(result.permissionChangeType).toEqual('broadening');
250245
const sanitizedDiff = result.formattedDiff!.replace(/\x1B\[[0-?]*[ -/]*[@-~]/g, '').trim();
251246
expect(sanitizedDiff).toBe(
252247
'Stack test-stack\n' +
@@ -265,59 +260,4 @@ describe('formatSecurityDiff', () => {
265260
'(NOTE: There may be security-related changes not in this list. See https://github.com/aws/aws-cdk/issues/1299)',
266261
);
267262
});
268-
269-
test('formats diff for any security change when approval level is ANY_CHANGE', () => {
270-
// WHEN
271-
const formatter = new DiffFormatter({
272-
ioHelper: mockIoHelper,
273-
templateInfo: {
274-
oldTemplate: {},
275-
newTemplate: mockNewTemplate,
276-
},
277-
});
278-
const result = formatter.formatSecurityDiff({
279-
requireApproval: RequireApproval.ANY_CHANGE,
280-
});
281-
282-
// THEN
283-
expect(result.formattedDiff).toBeDefined();
284-
expect(mockIoDefaultMessages.warning).toHaveBeenCalledWith(
285-
expect.stringContaining('potentially sensitive changes'),
286-
);
287-
const sanitizedDiff = result.formattedDiff!.replace(/\x1B\[[0-?]*[ -/]*[@-~]/g, '').trim();
288-
expect(sanitizedDiff).toBe(
289-
'Stack test-stack\n' +
290-
'IAM Statement Changes\n' +
291-
'┌───┬─────────────┬────────┬────────────────┬──────────────────────────────┬───────────┐\n' +
292-
'│ │ Resource │ Effect │ Action │ Principal │ Condition │\n' +
293-
'├───┼─────────────┼────────┼────────────────┼──────────────────────────────┼───────────┤\n' +
294-
'│ + │ ${Role.Arn} │ Allow │ sts:AssumeRole │ Service:lambda.amazonaws.com │ │\n' +
295-
'└───┴─────────────┴────────┴────────────────┴──────────────────────────────┴───────────┘\n' +
296-
'IAM Policy Changes\n' +
297-
'┌───┬──────────┬──────────────────────────────────────────────────────────────────┐\n' +
298-
'│ │ Resource │ Managed Policy ARN │\n' +
299-
'├───┼──────────┼──────────────────────────────────────────────────────────────────┤\n' +
300-
'│ + │ ${Role} │ arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole │\n' +
301-
'└───┴──────────┴──────────────────────────────────────────────────────────────────┘\n' +
302-
'(NOTE: There may be security-related changes not in this list. See https://github.com/aws/aws-cdk/issues/1299)',
303-
);
304-
});
305-
306-
test('returns empty object when approval level is NEVER', () => {
307-
// WHEN
308-
const formatter = new DiffFormatter({
309-
ioHelper: mockIoHelper,
310-
templateInfo: {
311-
oldTemplate: {},
312-
newTemplate: mockNewTemplate,
313-
},
314-
});
315-
const result = formatter.formatSecurityDiff({
316-
requireApproval: RequireApproval.NEVER,
317-
});
318-
319-
// THEN
320-
expect(result.formattedDiff).toBeUndefined();
321-
expect(mockIoDefaultMessages.warning).not.toHaveBeenCalled();
322-
});
323263
});

packages/@aws-cdk/toolkit-lib/lib/actions/diff/private/helpers.ts

+2-24
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,14 @@
1-
import type { DescribeChangeSetOutput } from '@aws-cdk/cloudformation-diff';
2-
import { fullDiff } from '@aws-cdk/cloudformation-diff';
31
import type * as cxapi from '@aws-cdk/cx-api';
42
import * as fs from 'fs-extra';
53
import * as uuid from 'uuid';
64
import type { ChangeSetDiffOptions, DiffOptions, LocalFileDiffOptions } from '..';
75
import { DiffMethod } from '..';
86
import type { Deployments, ResourcesToImport, IoHelper, SdkProvider, StackCollection, TemplateInfo } from '../../../api/shared-private';
97
import { ResourceMigrator, IO, removeNonImportResources, cfnApi } from '../../../api/shared-private';
10-
import { PermissionChangeType, ToolkitError } from '../../../api/shared-public';
8+
import { ToolkitError } from '../../../api/shared-public';
119
import { deserializeStructure, formatErrorMessage } from '../../../private/util';
1210

13-
export function makeTemplateInfos(
11+
export function prepareDiff(
1412
ioHelper: IoHelper,
1513
stacks: StackCollection,
1614
deployments: Deployments,
@@ -147,26 +145,6 @@ async function changeSetDiff(
147145
}
148146
}
149147

150-
/**
151-
* Return whether the diff has security-impacting changes that need confirmation.
152-
*/
153-
export function determinePermissionType(
154-
oldTemplate: any,
155-
newTemplate: cxapi.CloudFormationStackArtifact,
156-
changeSet?: DescribeChangeSetOutput,
157-
): PermissionChangeType {
158-
// @todo return a printable version of the full diff.
159-
const diff = fullDiff(oldTemplate, newTemplate.template, changeSet);
160-
161-
if (diff.permissionsBroadened) {
162-
return PermissionChangeType.BROADENING;
163-
} else if (diff.permissionsAnyChanges) {
164-
return PermissionChangeType.NON_BROADENING;
165-
} else {
166-
return PermissionChangeType.NONE;
167-
}
168-
}
169-
170148
/**
171149
* Appends all properties from obj2 to obj1.
172150
* obj2 values take priority in the case of collisions.

0 commit comments

Comments
 (0)