@beesolve/aws-accounts 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/applyLogic.js +239 -3
- package/dist/awsConfig.js +402 -23
- package/dist/cli.js +63 -26
- package/dist/commands/graveyard.js +27 -0
- package/dist/commands/remote.js +139 -38
- package/dist/commands/validate.js +45 -0
- package/dist/diff.js +255 -8
- package/dist/lambda/handler.js +8 -4
- package/dist/lambdaClient.js +5 -2
- package/dist/operations.js +83 -1
- package/dist/scanLogic.js +163 -7
- package/dist/state.js +162 -6
- package/dist-lambda/handler.mjs +647 -22
- package/dist-lambda/lambda.zip +0 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -11,7 +11,10 @@ import {
|
|
|
11
11
|
resolveAwsRegion
|
|
12
12
|
} from "./awsClientConfig.js";
|
|
13
13
|
import { consoleLogger } from "./logger.js";
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
runGraveyardCloseCommand,
|
|
16
|
+
runGraveyardCommand
|
|
17
|
+
} from "./commands/graveyard.js";
|
|
15
18
|
import { runProfileCommand } from "./commands/profile.js";
|
|
16
19
|
import { runRegenerateCommand } from "./commands/regenerate.js";
|
|
17
20
|
import { runValidateCommand } from "./commands/validate.js";
|
|
@@ -28,6 +31,7 @@ import {
|
|
|
28
31
|
exitCodeForCliErrorKind,
|
|
29
32
|
toUsageError
|
|
30
33
|
} from "./error.js";
|
|
34
|
+
import { readAwsContextFromFile, readPackageVersion } from "./awsConfig.js";
|
|
31
35
|
const commands = [
|
|
32
36
|
"bootstrap",
|
|
33
37
|
"scan",
|
|
@@ -55,6 +59,7 @@ async function main() {
|
|
|
55
59
|
"ignore-unsupported": { type: "boolean", default: false },
|
|
56
60
|
"allow-destructive": { type: "boolean", default: false },
|
|
57
61
|
refresh: { type: "boolean", default: false },
|
|
62
|
+
update: { type: "boolean", default: false },
|
|
58
63
|
"sso-start-url": { type: "string" },
|
|
59
64
|
"sso-session": { type: "string", default: "sso" },
|
|
60
65
|
help: { type: "boolean", default: false }
|
|
@@ -101,6 +106,21 @@ async function main() {
|
|
|
101
106
|
return;
|
|
102
107
|
}
|
|
103
108
|
if (command === "graveyard") {
|
|
109
|
+
const subcommand = args.positionals[1];
|
|
110
|
+
if (subcommand === "close") {
|
|
111
|
+
await runGraveyardCloseCommand({
|
|
112
|
+
logger,
|
|
113
|
+
cachePath: ".remote-state-cache.json",
|
|
114
|
+
contextPath
|
|
115
|
+
});
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
if (subcommand != null) {
|
|
119
|
+
printHelp(logger);
|
|
120
|
+
throw toUsageError(
|
|
121
|
+
`Unknown graveyard subcommand: "${subcommand}". Valid subcommands: close`
|
|
122
|
+
);
|
|
123
|
+
}
|
|
104
124
|
await runGraveyardCommand({
|
|
105
125
|
logger,
|
|
106
126
|
cachePath: ".remote-state-cache.json",
|
|
@@ -112,7 +132,9 @@ async function main() {
|
|
|
112
132
|
const ssoStartUrl = args.values["sso-start-url"] ?? process.env.AWS_SSO_START_URL;
|
|
113
133
|
if (ssoStartUrl == null) {
|
|
114
134
|
printHelp(logger);
|
|
115
|
-
throw toUsageError(
|
|
135
|
+
throw toUsageError(
|
|
136
|
+
"--sso-start-url is required for the profile command (or set AWS_SSO_START_URL)."
|
|
137
|
+
);
|
|
116
138
|
}
|
|
117
139
|
await runProfileCommand({
|
|
118
140
|
logger,
|
|
@@ -136,7 +158,8 @@ async function main() {
|
|
|
136
158
|
yes: args.values.yes ?? false,
|
|
137
159
|
refresh: args.values.refresh ?? false,
|
|
138
160
|
allowDestructive: args.values["allow-destructive"] ?? false,
|
|
139
|
-
ignoreUnsupported: args.values["ignore-unsupported"] ?? false
|
|
161
|
+
ignoreUnsupported: args.values["ignore-unsupported"] ?? false,
|
|
162
|
+
update: args.values.update ?? false
|
|
140
163
|
},
|
|
141
164
|
logger,
|
|
142
165
|
overwriteConfirmation,
|
|
@@ -147,25 +170,23 @@ async function main() {
|
|
|
147
170
|
ssoAdminClient: new SSOAdminClient(clientConfig)
|
|
148
171
|
};
|
|
149
172
|
if (command === "bootstrap") {
|
|
150
|
-
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
if (command === "
|
|
156
|
-
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
return runRemoteUpgrade(remoteInput);
|
|
173
|
+
await runRemoteBootstrap(remoteInput);
|
|
174
|
+
} else if (command === "scan") {
|
|
175
|
+
await runRemoteScan(remoteInput);
|
|
176
|
+
} else if (command === "init") {
|
|
177
|
+
await runRemoteInit(remoteInput);
|
|
178
|
+
} else if (command === "plan") {
|
|
179
|
+
await runRemotePlan(remoteInput);
|
|
180
|
+
} else if (command === "apply") {
|
|
181
|
+
await runRemoteApply(remoteInput);
|
|
182
|
+
} else if (command === "upgrade") {
|
|
183
|
+
await runRemoteUpgrade(remoteInput);
|
|
184
|
+
} else {
|
|
185
|
+
printHelp(logger);
|
|
186
|
+
process.exitCode = 1;
|
|
187
|
+
return;
|
|
166
188
|
}
|
|
167
|
-
|
|
168
|
-
process.exitCode = 1;
|
|
189
|
+
await printVersionBannerIfNeeded(logger);
|
|
169
190
|
}
|
|
170
191
|
function printHelp(logger) {
|
|
171
192
|
logger.log("@beesolve/aws-accounts");
|
|
@@ -174,15 +195,17 @@ function printHelp(logger) {
|
|
|
174
195
|
logger.log(
|
|
175
196
|
" npm run cli -- bootstrap [--profile <name>] [--region <region>] [--yes]"
|
|
176
197
|
);
|
|
198
|
+
logger.log(" npm run cli -- scan [--profile <name>] [--region <region>]");
|
|
177
199
|
logger.log(
|
|
178
|
-
" npm run cli --
|
|
200
|
+
" npm run cli -- init [--profile <name>] [--region <region>] [--yes]"
|
|
179
201
|
);
|
|
180
202
|
logger.log(
|
|
181
|
-
" npm run cli -- init [--profile <name>] [--region <region>] [--yes]"
|
|
203
|
+
" npm run cli -- init --update [--profile <name>] [--region <region>] [--yes]"
|
|
182
204
|
);
|
|
183
205
|
logger.log(" npm run cli -- regenerate [--yes]");
|
|
184
206
|
logger.log(" npm run cli -- validate");
|
|
185
207
|
logger.log(" npm run cli -- graveyard");
|
|
208
|
+
logger.log(" npm run cli -- graveyard close");
|
|
186
209
|
logger.log(
|
|
187
210
|
" npm run cli -- profile --sso-start-url <url> [--sso-session <name>] (env: AWS_SSO_START_URL)"
|
|
188
211
|
);
|
|
@@ -192,9 +215,7 @@ function printHelp(logger) {
|
|
|
192
215
|
logger.log(
|
|
193
216
|
" npm run cli -- apply [--profile <name>] [--region <region>] [--yes] [--allow-destructive] [--ignore-unsupported]"
|
|
194
217
|
);
|
|
195
|
-
logger.log(
|
|
196
|
-
" npm run cli -- upgrade [--profile <name>] [--region <region>]"
|
|
197
|
-
);
|
|
218
|
+
logger.log(" npm run cli -- upgrade [--profile <name>] [--region <region>]");
|
|
198
219
|
logger.log("");
|
|
199
220
|
logger.log("Environment fallback:");
|
|
200
221
|
logger.log(" AWS_PROFILE, AWS_REGION, AWS_DEFAULT_REGION");
|
|
@@ -227,6 +248,22 @@ function buildOverwriteConfirmation(props) {
|
|
|
227
248
|
}
|
|
228
249
|
};
|
|
229
250
|
}
|
|
251
|
+
async function printVersionBannerIfNeeded(logger) {
|
|
252
|
+
try {
|
|
253
|
+
const [context, currentVersion] = await Promise.all([
|
|
254
|
+
readAwsContextFromFile(contextPath),
|
|
255
|
+
readPackageVersion()
|
|
256
|
+
]);
|
|
257
|
+
const remoteVersion = context.deployment?.cliVersion;
|
|
258
|
+
if (remoteVersion != null && remoteVersion !== currentVersion) {
|
|
259
|
+
logger.log("");
|
|
260
|
+
logger.log(
|
|
261
|
+
`New version installed (local: ${currentVersion}, remote: ${remoteVersion}). Run upgrade then init --update to sync.`
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
} catch {
|
|
265
|
+
}
|
|
266
|
+
}
|
|
230
267
|
main().catch((error) => {
|
|
231
268
|
const classified = classifyCliError(error);
|
|
232
269
|
consoleLogger.error(`CLI ${classified.kind} error: ${classified.message}`);
|
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
import { readAwsContextFromFile } from "../awsConfig.js";
|
|
2
2
|
import { readStateCache } from "../remoteStateCache.js";
|
|
3
|
+
async function runGraveyardCloseCommand(props) {
|
|
4
|
+
const [cache, context] = await Promise.all([
|
|
5
|
+
readStateCache(props.cachePath),
|
|
6
|
+
readAwsContextFromFile(props.contextPath)
|
|
7
|
+
]);
|
|
8
|
+
if (cache == null) {
|
|
9
|
+
throw new Error(
|
|
10
|
+
`No remote state cache found at "${props.cachePath}". Run a scan or apply command first to populate the cache.`
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
const graveyardOuId = context.organization.graveyardOuId;
|
|
14
|
+
const eligible = cache.state.organization.accounts.filter((a) => a.parentId === graveyardOuId && a.status === "ACTIVE").sort((a, b) => a.name.localeCompare(b.name));
|
|
15
|
+
if (eligible.length === 0) {
|
|
16
|
+
props.logger.log("No accounts eligible for closure in Graveyard.");
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
props.logger.log(`${eligible.length} account(s) eligible for closure:
|
|
20
|
+
`);
|
|
21
|
+
for (const account of eligible) {
|
|
22
|
+
props.logger.log(`# ${account.name} (${account.id})`);
|
|
23
|
+
props.logger.log(
|
|
24
|
+
`aws organizations close-account --account-id ${account.id}`
|
|
25
|
+
);
|
|
26
|
+
props.logger.log("");
|
|
27
|
+
}
|
|
28
|
+
}
|
|
3
29
|
async function runGraveyardCommand(props) {
|
|
4
30
|
const [cache, context] = await Promise.all([
|
|
5
31
|
readStateCache(props.cachePath),
|
|
@@ -42,5 +68,6 @@ async function runGraveyardCommand(props) {
|
|
|
42
68
|
};
|
|
43
69
|
}
|
|
44
70
|
export {
|
|
71
|
+
runGraveyardCloseCommand,
|
|
45
72
|
runGraveyardCommand
|
|
46
73
|
};
|
package/dist/commands/remote.js
CHANGED
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
loadAwsConfigModelFromTsFile,
|
|
37
37
|
mapAwsConfigToState,
|
|
38
38
|
readAwsContextFromFile,
|
|
39
|
+
readPackageVersion,
|
|
39
40
|
regenerateTypesFromState,
|
|
40
41
|
writeAwsConfigFromState
|
|
41
42
|
} from "../awsConfig.js";
|
|
@@ -51,6 +52,7 @@ import {
|
|
|
51
52
|
import { applyReservedOuDeletionGuard } from "../reservedOuDeletion.js";
|
|
52
53
|
import { validateState } from "../state.js";
|
|
53
54
|
import { assertUnreachable, delay } from "../helpers.js";
|
|
55
|
+
import { toPreconditionError } from "../error.js";
|
|
54
56
|
import { sts, organizations, sso, identitystore, s3, logs, account, iam, lambda } from "@beesolve/iam-policy-ts";
|
|
55
57
|
const remoteCommandSchema = v.object({
|
|
56
58
|
subcommand: v.picklist(["bootstrap", "scan", "init", "plan", "apply", "upgrade"]),
|
|
@@ -60,7 +62,8 @@ const remoteCommandSchema = v.object({
|
|
|
60
62
|
yes: v.boolean(),
|
|
61
63
|
refresh: v.boolean(),
|
|
62
64
|
allowDestructive: v.boolean(),
|
|
63
|
-
ignoreUnsupported: v.boolean()
|
|
65
|
+
ignoreUnsupported: v.boolean(),
|
|
66
|
+
update: v.boolean()
|
|
64
67
|
})
|
|
65
68
|
});
|
|
66
69
|
const contextFilePath = "aws.context.json";
|
|
@@ -121,12 +124,14 @@ async function runRemoteBootstrap(input) {
|
|
|
121
124
|
context = await readAwsContextFromFile(contextFilePath);
|
|
122
125
|
} catch {
|
|
123
126
|
}
|
|
127
|
+
const cliVersionForBootstrap = await readPackageVersion();
|
|
124
128
|
const deployment = {
|
|
125
129
|
profile: input.profile ?? "",
|
|
126
130
|
region: resolvedRegion,
|
|
127
131
|
lambdaArn,
|
|
128
132
|
stateBucketName: bucketName,
|
|
129
|
-
stateCacheTtlSeconds: 300
|
|
133
|
+
stateCacheTtlSeconds: 300,
|
|
134
|
+
cliVersion: cliVersionForBootstrap
|
|
130
135
|
};
|
|
131
136
|
const updatedContext = context != null ? { ...context, deployment } : {
|
|
132
137
|
version: "1",
|
|
@@ -172,22 +177,7 @@ async function runRemoteBootstrap(input) {
|
|
|
172
177
|
input.logger.log(` Lambda ARN: ${lambdaArn}`);
|
|
173
178
|
input.logger.log(` State bucket: ${bucketName}`);
|
|
174
179
|
}
|
|
175
|
-
async function
|
|
176
|
-
const trustPolicy = JSON.stringify({
|
|
177
|
-
Version: "2012-10-17",
|
|
178
|
-
Statement: [
|
|
179
|
-
{
|
|
180
|
-
Effect: "Allow",
|
|
181
|
-
Principal: { Service: "lambda.amazonaws.com" },
|
|
182
|
-
Action: sts("AssumeRole")
|
|
183
|
-
}
|
|
184
|
-
]
|
|
185
|
-
});
|
|
186
|
-
const { roleArn } = await getOrCreateIamRole({
|
|
187
|
-
iamClient: props.iamClient,
|
|
188
|
-
trustPolicy,
|
|
189
|
-
logger: props.logger
|
|
190
|
-
});
|
|
180
|
+
async function applyLambdaRolePolicy(props) {
|
|
191
181
|
const inlinePolicy = JSON.stringify({
|
|
192
182
|
Version: "2012-10-17",
|
|
193
183
|
Statement: [
|
|
@@ -220,7 +210,12 @@ async function ensureIamRole(props) {
|
|
|
220
210
|
},
|
|
221
211
|
{
|
|
222
212
|
Effect: "Allow",
|
|
223
|
-
Action: [
|
|
213
|
+
Action: [
|
|
214
|
+
account("PutAccountName"),
|
|
215
|
+
account("GetAlternateContact"),
|
|
216
|
+
account("PutAlternateContact"),
|
|
217
|
+
account("DeleteAlternateContact")
|
|
218
|
+
],
|
|
224
219
|
Resource: "*"
|
|
225
220
|
}
|
|
226
221
|
]
|
|
@@ -232,6 +227,27 @@ async function ensureIamRole(props) {
|
|
|
232
227
|
PolicyDocument: inlinePolicy
|
|
233
228
|
})
|
|
234
229
|
);
|
|
230
|
+
}
|
|
231
|
+
async function ensureIamRole(props) {
|
|
232
|
+
const trustPolicy = JSON.stringify({
|
|
233
|
+
Version: "2012-10-17",
|
|
234
|
+
Statement: [
|
|
235
|
+
{
|
|
236
|
+
Effect: "Allow",
|
|
237
|
+
Principal: { Service: "lambda.amazonaws.com" },
|
|
238
|
+
Action: sts("AssumeRole")
|
|
239
|
+
}
|
|
240
|
+
]
|
|
241
|
+
});
|
|
242
|
+
const { roleArn } = await getOrCreateIamRole({
|
|
243
|
+
iamClient: props.iamClient,
|
|
244
|
+
trustPolicy,
|
|
245
|
+
logger: props.logger
|
|
246
|
+
});
|
|
247
|
+
await applyLambdaRolePolicy({
|
|
248
|
+
iamClient: props.iamClient,
|
|
249
|
+
bucketName: props.bucketName
|
|
250
|
+
});
|
|
235
251
|
return { roleArn };
|
|
236
252
|
}
|
|
237
253
|
async function getOrCreateIamRole(props) {
|
|
@@ -395,11 +411,27 @@ async function runRemoteScan(input) {
|
|
|
395
411
|
input.logger.log(` Groups: ${response.summary.groups}`);
|
|
396
412
|
input.logger.log(` Permission Sets: ${response.summary.permissionSets}`);
|
|
397
413
|
input.logger.log(` Account Assignments: ${response.summary.accountAssignments}`);
|
|
414
|
+
input.logger.log(` Policies: ${response.summary.policies}`);
|
|
415
|
+
input.logger.log(` Policy Attachments: ${response.summary.policyAttachments}`);
|
|
398
416
|
await writeStateCache(cachePath, response.state);
|
|
399
417
|
input.logger.log("State cache updated.");
|
|
400
418
|
}
|
|
401
419
|
async function runRemoteInit(input) {
|
|
402
|
-
const
|
|
420
|
+
const isUpdate = input.flags.update;
|
|
421
|
+
let existingConfig;
|
|
422
|
+
if (isUpdate) {
|
|
423
|
+
try {
|
|
424
|
+
existingConfig = await loadAwsConfigModelFromTsFile({
|
|
425
|
+
configPath: configFilePath,
|
|
426
|
+
typesPath: typesFilePath
|
|
427
|
+
});
|
|
428
|
+
} catch {
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
const [deployment, cliVersion] = await Promise.all([
|
|
432
|
+
readDeploymentFromContext(),
|
|
433
|
+
readPackageVersion()
|
|
434
|
+
]);
|
|
403
435
|
input.logger.log("Invoking remote scan...");
|
|
404
436
|
const result = await invokeLambda({
|
|
405
437
|
lambdaClient: input.lambdaClient,
|
|
@@ -420,14 +452,17 @@ async function runRemoteInit(input) {
|
|
|
420
452
|
input.logger.log(` Groups: ${response.summary.groups}`);
|
|
421
453
|
input.logger.log(` Permission Sets: ${response.summary.permissionSets}`);
|
|
422
454
|
input.logger.log(` Account Assignments: ${response.summary.accountAssignments}`);
|
|
455
|
+
input.logger.log(` Policies: ${response.summary.policies}`);
|
|
456
|
+
input.logger.log(` Policy Attachments: ${response.summary.policyAttachments}`);
|
|
423
457
|
await writeStateCache(cachePath, response.state);
|
|
424
458
|
input.logger.log("State cache updated.");
|
|
425
459
|
const context = await readAwsContextFromFile(contextFilePath);
|
|
426
460
|
const graveyardOu = response.state.organization.organizationalUnits.find(
|
|
427
461
|
(ou) => ou.name === "Graveyard"
|
|
428
462
|
);
|
|
429
|
-
const
|
|
430
|
-
|
|
463
|
+
const ordered = {
|
|
464
|
+
version: context.version,
|
|
465
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
431
466
|
organization: {
|
|
432
467
|
managementAccountId: context.organization.managementAccountId,
|
|
433
468
|
rootId: response.state.organization.rootId,
|
|
@@ -436,14 +471,8 @@ async function runRemoteInit(input) {
|
|
|
436
471
|
identityCenter: {
|
|
437
472
|
instanceArn: response.state.identityCenter.instanceArn,
|
|
438
473
|
identityStoreId: response.state.identityCenter.identityStoreId
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
const ordered = {
|
|
442
|
-
version: updatedContext.version,
|
|
443
|
-
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
444
|
-
organization: updatedContext.organization,
|
|
445
|
-
identityCenter: updatedContext.identityCenter,
|
|
446
|
-
deployment: updatedContext.deployment
|
|
474
|
+
},
|
|
475
|
+
deployment: { ...deployment, cliVersion }
|
|
447
476
|
};
|
|
448
477
|
await writeFile(contextFilePath, `${JSON.stringify(ordered, null, 2)}
|
|
449
478
|
`, "utf8");
|
|
@@ -453,12 +482,13 @@ async function runRemoteInit(input) {
|
|
|
453
482
|
configPath: configFilePath,
|
|
454
483
|
typesPath: typesFilePath,
|
|
455
484
|
logger: input.logger,
|
|
456
|
-
overwriteConfirmation: input.overwriteConfirmation
|
|
485
|
+
overwriteConfirmation: input.overwriteConfirmation,
|
|
486
|
+
existingConfig
|
|
457
487
|
});
|
|
458
488
|
const writtenFiles = configWriteResult.files.filter((f) => f.status === "written");
|
|
459
489
|
if (writtenFiles.length > 0) {
|
|
460
490
|
input.logger.log("");
|
|
461
|
-
input.logger.log("Init complete.");
|
|
491
|
+
input.logger.log(isUpdate ? "Init --update complete." : "Init complete.");
|
|
462
492
|
for (const file of writtenFiles) {
|
|
463
493
|
input.logger.log(` ${file.path}: ${file.status}`);
|
|
464
494
|
}
|
|
@@ -477,6 +507,7 @@ async function runRemotePlan(input) {
|
|
|
477
507
|
typesPath: typesFilePath
|
|
478
508
|
})
|
|
479
509
|
]);
|
|
510
|
+
warnIfRemotePoliciesNotInConfig({ currentState, config, logger: input.logger });
|
|
480
511
|
const desiredState = mapAwsConfigToState({
|
|
481
512
|
config,
|
|
482
513
|
currentState,
|
|
@@ -504,6 +535,7 @@ async function runRemoteApply(input) {
|
|
|
504
535
|
typesPath: typesFilePath
|
|
505
536
|
})
|
|
506
537
|
]);
|
|
538
|
+
warnIfRemotePoliciesNotInConfig({ currentState, config, logger: input.logger });
|
|
507
539
|
const desiredState = mapAwsConfigToState({
|
|
508
540
|
config,
|
|
509
541
|
currentState,
|
|
@@ -591,8 +623,11 @@ async function runRemoteApply(input) {
|
|
|
591
623
|
});
|
|
592
624
|
}
|
|
593
625
|
async function runRemoteUpgrade(input) {
|
|
594
|
-
const deployment = await
|
|
595
|
-
|
|
626
|
+
const [deployment, cliVersion, lambdaZip] = await Promise.all([
|
|
627
|
+
readDeploymentFromContext(),
|
|
628
|
+
readPackageVersion(),
|
|
629
|
+
readLambdaZip()
|
|
630
|
+
]);
|
|
596
631
|
input.logger.log(`Updating Lambda function code: ${deployment.lambdaArn}`);
|
|
597
632
|
await waitForLambdaReady(input.lambdaClient, deployment.lambdaArn);
|
|
598
633
|
const updateResult = await input.lambdaClient.send(
|
|
@@ -602,12 +637,51 @@ async function runRemoteUpgrade(input) {
|
|
|
602
637
|
})
|
|
603
638
|
);
|
|
604
639
|
const lastModified = updateResult.LastModified ?? "unknown";
|
|
605
|
-
input.logger.log(`
|
|
640
|
+
input.logger.log(`Lambda updated. Last modified: ${lastModified}`);
|
|
641
|
+
input.logger.log("Updating IAM role policy...");
|
|
642
|
+
await applyLambdaRolePolicy({
|
|
643
|
+
iamClient: input.iamClient,
|
|
644
|
+
bucketName: deployment.stateBucketName
|
|
645
|
+
});
|
|
646
|
+
input.logger.log("IAM role policy updated.");
|
|
647
|
+
const context = await readAwsContextFromFile(contextFilePath);
|
|
648
|
+
const ordered = {
|
|
649
|
+
version: context.version,
|
|
650
|
+
generatedAt: context.generatedAt,
|
|
651
|
+
organization: context.organization,
|
|
652
|
+
identityCenter: context.identityCenter,
|
|
653
|
+
deployment: { ...deployment, cliVersion }
|
|
654
|
+
};
|
|
655
|
+
await writeFile(contextFilePath, `${JSON.stringify(ordered, null, 2)}
|
|
656
|
+
`, "utf8");
|
|
657
|
+
input.logger.log("");
|
|
658
|
+
input.logger.log("Run init --update to sync your config with new remote features before using plan/apply.");
|
|
659
|
+
}
|
|
660
|
+
function warnIfRemotePoliciesNotInConfig(props) {
|
|
661
|
+
const remotePolicies = props.currentState.organization.policies ?? [];
|
|
662
|
+
const hasRemotePolicies = remotePolicies.length > 0;
|
|
663
|
+
const hasLocalPolicies = (props.config.policies?.serviceControlPolicies?.length ?? 0) > 0 || (props.config.policies?.resourceControlPolicies?.length ?? 0) > 0;
|
|
664
|
+
if (hasRemotePolicies && !hasLocalPolicies) {
|
|
665
|
+
props.logger.log("");
|
|
666
|
+
props.logger.log("Warning: remote state contains SCPs/RCPs not present in your config. Proceeding could delete them.");
|
|
667
|
+
props.logger.log("Run init --update to sync first.");
|
|
668
|
+
props.logger.log("");
|
|
669
|
+
}
|
|
606
670
|
}
|
|
607
671
|
async function readDeploymentFromContext() {
|
|
608
|
-
|
|
672
|
+
let context;
|
|
673
|
+
try {
|
|
674
|
+
context = await readAwsContextFromFile(contextFilePath);
|
|
675
|
+
} catch (err) {
|
|
676
|
+
if (err.code === "ENOENT") {
|
|
677
|
+
throw toPreconditionError(
|
|
678
|
+
"aws.context.json not found. Run `aws-accounts bootstrap` first."
|
|
679
|
+
);
|
|
680
|
+
}
|
|
681
|
+
throw err;
|
|
682
|
+
}
|
|
609
683
|
if (context.deployment == null) {
|
|
610
|
-
throw
|
|
684
|
+
throw toPreconditionError(
|
|
611
685
|
"No deployment found in aws.context.json. Run `aws-accounts bootstrap` first."
|
|
612
686
|
);
|
|
613
687
|
}
|
|
@@ -674,7 +748,7 @@ function displayPlan(props) {
|
|
|
674
748
|
}
|
|
675
749
|
}
|
|
676
750
|
function isDestructiveOperation(operation) {
|
|
677
|
-
return operation.kind === "deleteOu" || operation.kind === "removeAccount" || operation.kind === "deleteIdcUser" || operation.kind === "deleteIdcGroup" || operation.kind === "deleteIdcPermissionSet";
|
|
751
|
+
return operation.kind === "deleteOu" || operation.kind === "removeAccount" || operation.kind === "deleteIdcUser" || operation.kind === "deleteIdcGroup" || operation.kind === "deleteIdcPermissionSet" || operation.kind === "detachOrgPolicy" || operation.kind === "deleteOrgPolicy";
|
|
678
752
|
}
|
|
679
753
|
function formatOperationLine(operation) {
|
|
680
754
|
if (operation.kind === "moveAccount") {
|
|
@@ -765,6 +839,33 @@ function formatOperationLine(operation) {
|
|
|
765
839
|
const duration = operation.sessionDuration ?? "default";
|
|
766
840
|
return ` update IdC permission set session duration "${operation.permissionSetName}" -> ${duration}`;
|
|
767
841
|
}
|
|
842
|
+
if (operation.kind === "createOrgPolicy") {
|
|
843
|
+
return ` create org policy "${operation.policyName}" (${operation.policyType})`;
|
|
844
|
+
}
|
|
845
|
+
if (operation.kind === "updateOrgPolicyContent") {
|
|
846
|
+
return ` update org policy content "${operation.policyName}"`;
|
|
847
|
+
}
|
|
848
|
+
if (operation.kind === "updateOrgPolicyDescription") {
|
|
849
|
+
return ` update org policy description "${operation.policyName}"`;
|
|
850
|
+
}
|
|
851
|
+
if (operation.kind === "attachOrgPolicy") {
|
|
852
|
+
return ` attach org policy "${operation.policyName}" to "${operation.targetName}"`;
|
|
853
|
+
}
|
|
854
|
+
if (operation.kind === "detachOrgPolicy") {
|
|
855
|
+
return ` [destructive] detach org policy "${operation.policyName}" from "${operation.targetName}"`;
|
|
856
|
+
}
|
|
857
|
+
if (operation.kind === "deleteOrgPolicy") {
|
|
858
|
+
return ` [destructive] delete org policy "${operation.policyName}"`;
|
|
859
|
+
}
|
|
860
|
+
if (operation.kind === "putAlternateContact") {
|
|
861
|
+
return ` set ${operation.contactType} alternate contact for "${operation.accountName}" (${operation.accountId})`;
|
|
862
|
+
}
|
|
863
|
+
if (operation.kind === "deleteAlternateContact") {
|
|
864
|
+
return ` [destructive] delete ${operation.contactType} alternate contact for "${operation.accountName}" (${operation.accountId})`;
|
|
865
|
+
}
|
|
866
|
+
if (operation.kind === "setIdcAccessControlAttributes") {
|
|
867
|
+
return ` set IdC access control attributes (${operation.attributes.length} attribute(s))`;
|
|
868
|
+
}
|
|
768
869
|
assertUnreachable(operation, "Unsupported operation kind in formatOperationLine.");
|
|
769
870
|
}
|
|
770
871
|
function formatPrincipalLabel(principalType, principalName) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { loadAwsConfigModelFromTsFile } from "../awsConfig.js";
|
|
2
2
|
const INLINE_POLICY_MAX_CHARS = 10240;
|
|
3
|
+
const ORG_POLICY_CONTENT_MAX_BYTES = 5120;
|
|
3
4
|
async function runValidateCommand(input) {
|
|
4
5
|
const configPath = input.configPath ?? "aws.config.ts";
|
|
5
6
|
const typesPath = input.typesPath ?? "aws.config.types.ts";
|
|
@@ -14,6 +15,8 @@ async function runValidateCommand(input) {
|
|
|
14
15
|
checkCircularOuReferences(config, errors);
|
|
15
16
|
checkAssignmentPrincipals(config, errors);
|
|
16
17
|
checkInlinePolicySizes(config, errors);
|
|
18
|
+
checkOrgPolicySizes(config, errors);
|
|
19
|
+
checkOrgPolicyTargets(config, errors);
|
|
17
20
|
if (errors.length > 0) {
|
|
18
21
|
for (const error of errors) {
|
|
19
22
|
input.logger.log(`Error: ${error}`);
|
|
@@ -62,6 +65,48 @@ function checkAssignmentPrincipals(config, errors) {
|
|
|
62
65
|
}
|
|
63
66
|
}
|
|
64
67
|
}
|
|
68
|
+
function checkOrgPolicySizes(config, errors) {
|
|
69
|
+
for (const policy of config.policies?.serviceControlPolicies ?? []) {
|
|
70
|
+
const contentBytes = Buffer.byteLength(JSON.stringify(policy.content), "utf8");
|
|
71
|
+
if (contentBytes > ORG_POLICY_CONTENT_MAX_BYTES) {
|
|
72
|
+
errors.push(
|
|
73
|
+
`Service control policy "${policy.name}" content is ${contentBytes} bytes (limit: ${ORG_POLICY_CONTENT_MAX_BYTES}).`
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
for (const policy of config.policies?.resourceControlPolicies ?? []) {
|
|
78
|
+
const contentBytes = Buffer.byteLength(JSON.stringify(policy.content), "utf8");
|
|
79
|
+
if (contentBytes > ORG_POLICY_CONTENT_MAX_BYTES) {
|
|
80
|
+
errors.push(
|
|
81
|
+
`Resource control policy "${policy.name}" content is ${contentBytes} bytes (limit: ${ORG_POLICY_CONTENT_MAX_BYTES}).`
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function checkOrgPolicyTargets(config, errors) {
|
|
87
|
+
const ouNames = new Set(config.organizationalUnits.map((ou) => ou.name));
|
|
88
|
+
const accountNames = new Set(
|
|
89
|
+
config.organizationalUnits.flatMap((ou) => ou.accounts.map((a) => a.name))
|
|
90
|
+
);
|
|
91
|
+
for (const policy of config.policies?.serviceControlPolicies ?? []) {
|
|
92
|
+
for (const target of policy.targets) {
|
|
93
|
+
if (target !== "root" && !ouNames.has(target) && !accountNames.has(target)) {
|
|
94
|
+
errors.push(
|
|
95
|
+
`Service control policy "${policy.name}" references unknown target "${target}".`
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
for (const policy of config.policies?.resourceControlPolicies ?? []) {
|
|
101
|
+
for (const target of policy.targets) {
|
|
102
|
+
if (target !== "root" && !ouNames.has(target) && !accountNames.has(target)) {
|
|
103
|
+
errors.push(
|
|
104
|
+
`Resource control policy "${policy.name}" references unknown target "${target}".`
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
65
110
|
function checkInlinePolicySizes(config, errors) {
|
|
66
111
|
for (const ps of config.permissionSets) {
|
|
67
112
|
if (ps.inlinePolicy == null) {
|