@beesolve/aws-accounts 1.0.7 → 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 +80 -11
- package/dist/applyLogic.js +288 -19
- package/dist/awsConfig.js +414 -32
- package/dist/cli.js +95 -25
- package/dist/commands/graveyard.js +27 -0
- package/dist/commands/profile.js +116 -0
- package/dist/commands/remote.js +152 -47
- package/dist/commands/validate.js +125 -0
- package/dist/diff.js +278 -22
- package/dist/lambda/handler.js +8 -4
- package/dist/lambdaClient.js +5 -2
- package/dist/operations.js +91 -2
- package/dist/scanLogic.js +164 -7
- package/dist/state.js +164 -7
- package/dist-lambda/handler.mjs +707 -40
- package/dist-lambda/lambda.zip +0 -0
- package/package.json +1 -1
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,7 +52,8 @@ import {
|
|
|
51
52
|
import { applyReservedOuDeletionGuard } from "../reservedOuDeletion.js";
|
|
52
53
|
import { validateState } from "../state.js";
|
|
53
54
|
import { assertUnreachable, delay } from "../helpers.js";
|
|
54
|
-
import {
|
|
55
|
+
import { toPreconditionError } from "../error.js";
|
|
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"]),
|
|
57
59
|
profile: v.optional(v.string()),
|
|
@@ -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,38 +177,23 @@ 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: iam.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: [
|
|
194
184
|
{
|
|
195
185
|
Effect: "Allow",
|
|
196
|
-
Action:
|
|
186
|
+
Action: organizations("*"),
|
|
197
187
|
Resource: "*"
|
|
198
188
|
},
|
|
199
189
|
{
|
|
200
190
|
Effect: "Allow",
|
|
201
|
-
Action: [
|
|
191
|
+
Action: [sso("*"), identitystore("*")],
|
|
202
192
|
Resource: "*"
|
|
203
193
|
},
|
|
204
194
|
{
|
|
205
195
|
Effect: "Allow",
|
|
206
|
-
Action: [
|
|
196
|
+
Action: [s3("GetObject"), s3("PutObject"), s3("ListBucket")],
|
|
207
197
|
Resource: [
|
|
208
198
|
`arn:aws:s3:::${props.bucketName}`,
|
|
209
199
|
`arn:aws:s3:::${props.bucketName}/*`
|
|
@@ -212,15 +202,20 @@ async function ensureIamRole(props) {
|
|
|
212
202
|
{
|
|
213
203
|
Effect: "Allow",
|
|
214
204
|
Action: [
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
205
|
+
logs("CreateLogGroup"),
|
|
206
|
+
logs("CreateLogStream"),
|
|
207
|
+
logs("PutLogEvents")
|
|
218
208
|
],
|
|
219
209
|
Resource: "arn:aws:logs:*:*:*"
|
|
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") {
|
|
@@ -761,6 +835,37 @@ function formatOperationLine(operation) {
|
|
|
761
835
|
if (operation.kind === "revokeIdcAccountAssignment") {
|
|
762
836
|
return ` revoke IdC assignment "${operation.permissionSetName}" from ${formatPrincipalLabel(operation.principalType, operation.principalName)} on "${operation.accountName}"`;
|
|
763
837
|
}
|
|
838
|
+
if (operation.kind === "updateIdcPermissionSetSessionDuration") {
|
|
839
|
+
const duration = operation.sessionDuration ?? "default";
|
|
840
|
+
return ` update IdC permission set session duration "${operation.permissionSetName}" -> ${duration}`;
|
|
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
|
+
}
|
|
764
869
|
assertUnreachable(operation, "Unsupported operation kind in formatOperationLine.");
|
|
765
870
|
}
|
|
766
871
|
function formatPrincipalLabel(principalType, principalName) {
|
|
@@ -817,7 +922,7 @@ async function ensureOrganizationManagementPermissionSet(props) {
|
|
|
817
922
|
Version: "2012-10-17",
|
|
818
923
|
Statement: [{
|
|
819
924
|
Effect: "Allow",
|
|
820
|
-
Action: [
|
|
925
|
+
Action: [organizations("*"), sso("*"), identitystore("*"), account("*"), iam("*")],
|
|
821
926
|
Resource: "*"
|
|
822
927
|
}]
|
|
823
928
|
});
|
|
@@ -867,7 +972,7 @@ async function ensureOrganizationRemoteManagementPermissionSet(props) {
|
|
|
867
972
|
Version: "2012-10-17",
|
|
868
973
|
Statement: [{
|
|
869
974
|
Effect: "Allow",
|
|
870
|
-
Action: [
|
|
975
|
+
Action: [lambda("InvokeFunction")],
|
|
871
976
|
Resource: props.lambdaArn
|
|
872
977
|
}]
|
|
873
978
|
});
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { loadAwsConfigModelFromTsFile } from "../awsConfig.js";
|
|
2
|
+
const INLINE_POLICY_MAX_CHARS = 10240;
|
|
3
|
+
const ORG_POLICY_CONTENT_MAX_BYTES = 5120;
|
|
4
|
+
async function runValidateCommand(input) {
|
|
5
|
+
const configPath = input.configPath ?? "aws.config.ts";
|
|
6
|
+
const typesPath = input.typesPath ?? "aws.config.types.ts";
|
|
7
|
+
let config;
|
|
8
|
+
try {
|
|
9
|
+
config = await loadAwsConfigModelFromTsFile({ configPath, typesPath });
|
|
10
|
+
} catch (error) {
|
|
11
|
+
input.logger.log(`Config error: ${error instanceof Error ? error.message : String(error)}`);
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
const errors = [];
|
|
15
|
+
checkCircularOuReferences(config, errors);
|
|
16
|
+
checkAssignmentPrincipals(config, errors);
|
|
17
|
+
checkInlinePolicySizes(config, errors);
|
|
18
|
+
checkOrgPolicySizes(config, errors);
|
|
19
|
+
checkOrgPolicyTargets(config, errors);
|
|
20
|
+
if (errors.length > 0) {
|
|
21
|
+
for (const error of errors) {
|
|
22
|
+
input.logger.log(`Error: ${error}`);
|
|
23
|
+
}
|
|
24
|
+
input.logger.log(`
|
|
25
|
+
Validation failed with ${errors.length} error(s).`);
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
input.logger.log("Config is valid.");
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
function checkCircularOuReferences(config, errors) {
|
|
32
|
+
const parentByName = new Map(
|
|
33
|
+
config.organizationalUnits.map((ou) => [ou.name, ou.parentName])
|
|
34
|
+
);
|
|
35
|
+
const confirmed = /* @__PURE__ */ new Set();
|
|
36
|
+
for (const ou of config.organizationalUnits) {
|
|
37
|
+
if (ou.name === "root" || confirmed.has(ou.name)) {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
const visited = /* @__PURE__ */ new Set();
|
|
41
|
+
let current = ou.name;
|
|
42
|
+
while (current != null) {
|
|
43
|
+
if (visited.has(current)) {
|
|
44
|
+
errors.push(`Circular OU reference detected: "${current}" is its own ancestor.`);
|
|
45
|
+
confirmed.add(current);
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
visited.add(current);
|
|
49
|
+
current = parentByName.get(current) ?? null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function checkAssignmentPrincipals(config, errors) {
|
|
54
|
+
for (const assignment of config.assignments) {
|
|
55
|
+
const hasGroup = assignment.group != null;
|
|
56
|
+
const hasUser = assignment.user != null;
|
|
57
|
+
if (hasGroup && hasUser) {
|
|
58
|
+
errors.push(
|
|
59
|
+
`Assignment for permission set "${assignment.permissionSet}" specifies both "group" and "user" \u2014 only one is allowed.`
|
|
60
|
+
);
|
|
61
|
+
} else if (!hasGroup && !hasUser) {
|
|
62
|
+
errors.push(
|
|
63
|
+
`Assignment for permission set "${assignment.permissionSet}" has no principal \u2014 "group" or "user" is required.`
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
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
|
+
}
|
|
110
|
+
function checkInlinePolicySizes(config, errors) {
|
|
111
|
+
for (const ps of config.permissionSets) {
|
|
112
|
+
if (ps.inlinePolicy == null) {
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
const length = JSON.stringify(ps.inlinePolicy).length;
|
|
116
|
+
if (length > INLINE_POLICY_MAX_CHARS) {
|
|
117
|
+
errors.push(
|
|
118
|
+
`Permission set "${ps.name}" inline policy is ${length} characters (limit: ${INLINE_POLICY_MAX_CHARS}).`
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
export {
|
|
124
|
+
runValidateCommand
|
|
125
|
+
};
|