@beesolve/aws-accounts 1.2.0 → 1.3.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/dist/cli.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { parseArgs } from "node:util";
2
2
  import { createInterface } from "node:readline/promises";
3
+ import { basename } from "node:path";
3
4
  import { S3Client } from "@aws-sdk/client-s3";
4
5
  import { IAMClient } from "@aws-sdk/client-iam";
5
6
  import { LambdaClient } from "@aws-sdk/client-lambda";
@@ -24,13 +25,15 @@ import {
24
25
  runRemoteInit,
25
26
  runRemotePlan,
26
27
  runRemoteApply,
27
- runRemoteUpgrade
28
+ runRemoteUpgrade,
29
+ runRemoteDrift
28
30
  } from "./commands/remote.js";
29
31
  import {
30
32
  classifyCliError,
31
33
  exitCodeForCliErrorKind,
32
34
  toUsageError
33
35
  } from "./error.js";
36
+ import { assertUnreachable } from "./helpers.js";
34
37
  import { readAwsContextFromFile, readPackageVersion } from "./awsConfig.js";
35
38
  const commands = [
36
39
  "bootstrap",
@@ -42,7 +45,8 @@ const commands = [
42
45
  "profile",
43
46
  "plan",
44
47
  "apply",
45
- "upgrade"
48
+ "upgrade",
49
+ "drift"
46
50
  ];
47
51
  function isCommandName(value) {
48
52
  return commands.includes(value);
@@ -171,51 +175,73 @@ async function main() {
171
175
  };
172
176
  if (command === "bootstrap") {
173
177
  await runRemoteBootstrap(remoteInput);
174
- } else if (command === "scan") {
178
+ await printVersionBannerIfNeeded(logger);
179
+ return;
180
+ }
181
+ if (command === "scan") {
175
182
  await runRemoteScan(remoteInput);
176
- } else if (command === "init") {
183
+ await printVersionBannerIfNeeded(logger);
184
+ return;
185
+ }
186
+ if (command === "init") {
177
187
  await runRemoteInit(remoteInput);
178
- } else if (command === "plan") {
188
+ await printVersionBannerIfNeeded(logger);
189
+ return;
190
+ }
191
+ if (command === "plan") {
179
192
  await runRemotePlan(remoteInput);
180
- } else if (command === "apply") {
193
+ await printVersionBannerIfNeeded(logger);
194
+ return;
195
+ }
196
+ if (command === "apply") {
181
197
  await runRemoteApply(remoteInput);
182
- } else if (command === "upgrade") {
198
+ await printVersionBannerIfNeeded(logger);
199
+ return;
200
+ }
201
+ if (command === "upgrade") {
183
202
  await runRemoteUpgrade(remoteInput);
184
- } else {
185
- printHelp(logger);
186
- process.exitCode = 1;
203
+ await printVersionBannerIfNeeded(logger);
187
204
  return;
188
205
  }
189
- await printVersionBannerIfNeeded(logger);
206
+ if (command === "drift") {
207
+ await runRemoteDrift(remoteInput);
208
+ await printVersionBannerIfNeeded(logger);
209
+ return;
210
+ }
211
+ assertUnreachable(command, `Unhandled remote command: "${command}"`);
190
212
  }
191
213
  function printHelp(logger) {
214
+ const cmd = basename(process.argv[1], ".js");
192
215
  logger.log("@beesolve/aws-accounts");
193
216
  logger.log("");
194
217
  logger.log("Usage:");
195
218
  logger.log(
196
- " npm run cli -- bootstrap [--profile <name>] [--region <region>] [--yes]"
219
+ ` ${cmd} bootstrap [--profile <name>] [--region <region>] [--yes]`
220
+ );
221
+ logger.log(` ${cmd} scan [--profile <name>] [--region <region>]`);
222
+ logger.log(
223
+ ` ${cmd} init [--profile <name>] [--region <region>] [--yes]`
197
224
  );
198
- logger.log(" npm run cli -- scan [--profile <name>] [--region <region>]");
199
225
  logger.log(
200
- " npm run cli -- init [--profile <name>] [--region <region>] [--yes]"
226
+ ` ${cmd} init --update [--profile <name>] [--region <region>] [--yes]`
201
227
  );
228
+ logger.log(` ${cmd} regenerate [--yes]`);
229
+ logger.log(` ${cmd} validate`);
230
+ logger.log(` ${cmd} graveyard`);
231
+ logger.log(` ${cmd} graveyard close`);
202
232
  logger.log(
203
- " npm run cli -- init --update [--profile <name>] [--region <region>] [--yes]"
233
+ ` ${cmd} profile --sso-start-url <url> [--sso-session <name>] (env: AWS_SSO_START_URL)`
204
234
  );
205
- logger.log(" npm run cli -- regenerate [--yes]");
206
- logger.log(" npm run cli -- validate");
207
- logger.log(" npm run cli -- graveyard");
208
- logger.log(" npm run cli -- graveyard close");
209
235
  logger.log(
210
- " npm run cli -- profile --sso-start-url <url> [--sso-session <name>] (env: AWS_SSO_START_URL)"
236
+ ` ${cmd} plan [--profile <name>] [--region <region>] [--refresh]`
211
237
  );
212
238
  logger.log(
213
- " npm run cli -- plan [--profile <name>] [--region <region>] [--refresh]"
239
+ ` ${cmd} apply [--profile <name>] [--region <region>] [--yes] [--allow-destructive] [--ignore-unsupported]`
214
240
  );
241
+ logger.log(` ${cmd} upgrade [--profile <name>] [--region <region>]`);
215
242
  logger.log(
216
- " npm run cli -- apply [--profile <name>] [--region <region>] [--yes] [--allow-destructive] [--ignore-unsupported]"
243
+ ` ${cmd} drift [--profile <name>] [--region <region>] [--refresh]`
217
244
  );
218
- logger.log(" npm run cli -- upgrade [--profile <name>] [--region <region>]");
219
245
  logger.log("");
220
246
  logger.log("Environment fallback:");
221
247
  logger.log(" AWS_PROFILE, AWS_REGION, AWS_DEFAULT_REGION");
@@ -55,7 +55,7 @@ import { assertUnreachable, delay } from "../helpers.js";
55
55
  import { toPreconditionError } from "../error.js";
56
56
  import { sts, organizations, sso, identitystore, s3, logs, account, iam, lambda } from "@beesolve/iam-policy-ts";
57
57
  const remoteCommandSchema = v.object({
58
- subcommand: v.picklist(["bootstrap", "scan", "init", "plan", "apply", "upgrade"]),
58
+ subcommand: v.picklist(["bootstrap", "scan", "init", "plan", "apply", "upgrade", "drift"]),
59
59
  profile: v.optional(v.string()),
60
60
  region: v.optional(v.string()),
61
61
  flags: v.object({
@@ -657,10 +657,61 @@ async function runRemoteUpgrade(input) {
657
657
  input.logger.log("");
658
658
  input.logger.log("Run init --update to sync your config with new remote features before using plan/apply.");
659
659
  }
660
+ async function runRemoteDrift(input) {
661
+ const deployment = await readDeploymentFromContext();
662
+ const baseline = await fetchCurrentState({
663
+ input,
664
+ deployment
665
+ });
666
+ const clientConfig = buildAwsClientConfig({
667
+ profile: input.profile ?? (deployment.profile || void 0),
668
+ region: input.region ?? (deployment.region || void 0)
669
+ });
670
+ const lambdaClient = new LambdaClient(clientConfig);
671
+ input.logger.log("Scanning live AWS state...");
672
+ const result = await invokeLambda({
673
+ lambdaClient,
674
+ lambdaArn: deployment.lambdaArn,
675
+ payload: { action: "scan" }
676
+ });
677
+ if (!result.ok) {
678
+ throw new Error(formatLambdaError(result.error));
679
+ }
680
+ const response = result.response;
681
+ if (!("action" in response) || response.action !== "scan") {
682
+ throw new Error("Unexpected response from Lambda scan action.");
683
+ }
684
+ const liveState = response.state;
685
+ await writeStateCache(cachePath, liveState);
686
+ const plan = diffStates({
687
+ current: baseline,
688
+ next: liveState
689
+ });
690
+ displayDrift({ plan, logger: input.logger });
691
+ }
692
+ function displayDrift(props) {
693
+ const driftOperations = props.plan.operations.filter(
694
+ (operation) => operation.kind !== "provisionIdcPermissionSet"
695
+ );
696
+ if (driftOperations.length === 0 && props.plan.unsupported.length === 0) {
697
+ props.logger.log("No drift.");
698
+ return;
699
+ }
700
+ props.logger.log(`Drift: ${driftOperations.length} change(s) detected since last scan`);
701
+ for (const operation of driftOperations) {
702
+ props.logger.log(formatOperationLine(operation));
703
+ }
704
+ if (props.plan.unsupported.length > 0) {
705
+ props.logger.log("Unsupported diffs:");
706
+ for (const diff of props.plan.unsupported) {
707
+ props.logger.log(` - ${diff.description} [${diff.category}]`);
708
+ }
709
+ }
710
+ }
660
711
  function warnIfRemotePoliciesNotInConfig(props) {
661
712
  const remotePolicies = props.currentState.organization.policies ?? [];
662
713
  const hasRemotePolicies = remotePolicies.length > 0;
663
- const hasLocalPolicies = (props.config.policies?.serviceControlPolicies?.length ?? 0) > 0 || (props.config.policies?.resourceControlPolicies?.length ?? 0) > 0;
714
+ const hasLocalPolicies = props.config.policies.serviceControlPolicies.length > 0 || props.config.policies.resourceControlPolicies.length > 0;
664
715
  if (hasRemotePolicies && !hasLocalPolicies) {
665
716
  props.logger.log("");
666
717
  props.logger.log("Warning: remote state contains SCPs/RCPs not present in your config. Proceeding could delete them.");
@@ -748,7 +799,7 @@ function displayPlan(props) {
748
799
  }
749
800
  }
750
801
  function isDestructiveOperation(operation) {
751
- return operation.kind === "deleteOu" || operation.kind === "removeAccount" || operation.kind === "deleteIdcUser" || operation.kind === "deleteIdcGroup" || operation.kind === "deleteIdcPermissionSet" || operation.kind === "detachOrgPolicy" || operation.kind === "deleteOrgPolicy";
802
+ return operation.kind === "deleteOu" || operation.kind === "removeAccount" || operation.kind === "deleteIdcUser" || operation.kind === "deleteIdcGroup" || operation.kind === "deleteIdcPermissionSet" || operation.kind === "deleteIdcPermissionSetPermissionsBoundary" || operation.kind === "detachOrgPolicy" || operation.kind === "deleteOrgPolicy" || operation.kind === "deregisterDelegatedAdministrator";
752
803
  }
753
804
  function formatOperationLine(operation) {
754
805
  if (operation.kind === "moveAccount") {
@@ -826,6 +877,14 @@ function formatOperationLine(operation) {
826
877
  if (operation.kind === "provisionIdcPermissionSet") {
827
878
  return ` provision IdC permission set "${operation.permissionSetName}" to all provisioned accounts`;
828
879
  }
880
+ if (operation.kind === "putIdcPermissionSetPermissionsBoundary") {
881
+ const b = operation.permissionsBoundary;
882
+ const label = "managedPolicyArn" in b ? b.managedPolicyArn : `${b.customerManagedPolicyPath}${b.customerManagedPolicyName}`;
883
+ return ` put permissions boundary "${label}" on IdC permission set "${operation.permissionSetName}"`;
884
+ }
885
+ if (operation.kind === "deleteIdcPermissionSetPermissionsBoundary") {
886
+ return ` [destructive] delete permissions boundary from IdC permission set "${operation.permissionSetName}"`;
887
+ }
829
888
  if (operation.kind === "removeIdcGroupMembership") {
830
889
  return ` remove user "${operation.userName}" from IdC group "${operation.groupDisplayName}"`;
831
890
  }
@@ -866,6 +925,12 @@ function formatOperationLine(operation) {
866
925
  if (operation.kind === "setIdcAccessControlAttributes") {
867
926
  return ` set IdC access control attributes (${operation.attributes.length} attribute(s))`;
868
927
  }
928
+ if (operation.kind === "registerDelegatedAdministrator") {
929
+ return ` register delegated administrator "${operation.accountName}" (${operation.accountId}) for ${operation.servicePrincipal}`;
930
+ }
931
+ if (operation.kind === "deregisterDelegatedAdministrator") {
932
+ return ` [destructive] deregister delegated administrator "${operation.accountName}" (${operation.accountId}) for ${operation.servicePrincipal}`;
933
+ }
869
934
  assertUnreachable(operation, "Unsupported operation kind in formatOperationLine.");
870
935
  }
871
936
  function formatPrincipalLabel(principalType, principalName) {
@@ -1077,6 +1142,7 @@ async function waitForLambdaReady(lambdaClient, functionName) {
1077
1142
  export {
1078
1143
  runRemoteApply,
1079
1144
  runRemoteBootstrap,
1145
+ runRemoteDrift,
1080
1146
  runRemoteInit,
1081
1147
  runRemotePlan,
1082
1148
  runRemoteScan,
@@ -17,6 +17,7 @@ async function runValidateCommand(input) {
17
17
  checkInlinePolicySizes(config, errors);
18
18
  checkOrgPolicySizes(config, errors);
19
19
  checkOrgPolicyTargets(config, errors);
20
+ checkDelegatedAdministratorDuplicates(config, errors);
20
21
  if (errors.length > 0) {
21
22
  for (const error of errors) {
22
23
  input.logger.log(`Error: ${error}`);
@@ -66,7 +67,7 @@ function checkAssignmentPrincipals(config, errors) {
66
67
  }
67
68
  }
68
69
  function checkOrgPolicySizes(config, errors) {
69
- for (const policy of config.policies?.serviceControlPolicies ?? []) {
70
+ for (const policy of config.policies.serviceControlPolicies) {
70
71
  const contentBytes = Buffer.byteLength(JSON.stringify(policy.content), "utf8");
71
72
  if (contentBytes > ORG_POLICY_CONTENT_MAX_BYTES) {
72
73
  errors.push(
@@ -74,7 +75,7 @@ function checkOrgPolicySizes(config, errors) {
74
75
  );
75
76
  }
76
77
  }
77
- for (const policy of config.policies?.resourceControlPolicies ?? []) {
78
+ for (const policy of config.policies.resourceControlPolicies) {
78
79
  const contentBytes = Buffer.byteLength(JSON.stringify(policy.content), "utf8");
79
80
  if (contentBytes > ORG_POLICY_CONTENT_MAX_BYTES) {
80
81
  errors.push(
@@ -82,13 +83,21 @@ function checkOrgPolicySizes(config, errors) {
82
83
  );
83
84
  }
84
85
  }
86
+ for (const policy of config.policies.backupPolicies) {
87
+ const contentBytes = Buffer.byteLength(JSON.stringify(policy.content), "utf8");
88
+ if (contentBytes > ORG_POLICY_CONTENT_MAX_BYTES) {
89
+ errors.push(
90
+ `Backup policy "${policy.name}" content is ${contentBytes} bytes (limit: ${ORG_POLICY_CONTENT_MAX_BYTES}).`
91
+ );
92
+ }
93
+ }
85
94
  }
86
95
  function checkOrgPolicyTargets(config, errors) {
87
96
  const ouNames = new Set(config.organizationalUnits.map((ou) => ou.name));
88
97
  const accountNames = new Set(
89
98
  config.organizationalUnits.flatMap((ou) => ou.accounts.map((a) => a.name))
90
99
  );
91
- for (const policy of config.policies?.serviceControlPolicies ?? []) {
100
+ for (const policy of config.policies.serviceControlPolicies) {
92
101
  for (const target of policy.targets) {
93
102
  if (target !== "root" && !ouNames.has(target) && !accountNames.has(target)) {
94
103
  errors.push(
@@ -97,7 +106,7 @@ function checkOrgPolicyTargets(config, errors) {
97
106
  }
98
107
  }
99
108
  }
100
- for (const policy of config.policies?.resourceControlPolicies ?? []) {
109
+ for (const policy of config.policies.resourceControlPolicies) {
101
110
  for (const target of policy.targets) {
102
111
  if (target !== "root" && !ouNames.has(target) && !accountNames.has(target)) {
103
112
  errors.push(
@@ -106,6 +115,27 @@ function checkOrgPolicyTargets(config, errors) {
106
115
  }
107
116
  }
108
117
  }
118
+ for (const policy of config.policies.backupPolicies) {
119
+ for (const target of policy.targets) {
120
+ if (target !== "root" && !ouNames.has(target) && !accountNames.has(target)) {
121
+ errors.push(
122
+ `Backup policy "${policy.name}" references unknown target "${target}".`
123
+ );
124
+ }
125
+ }
126
+ }
127
+ }
128
+ function checkDelegatedAdministratorDuplicates(config, errors) {
129
+ const seen = /* @__PURE__ */ new Set();
130
+ for (const da of config.delegatedAdministrators) {
131
+ const key = `${da.account}|${da.servicePrincipal}`;
132
+ if (seen.has(key)) {
133
+ errors.push(
134
+ `Duplicate delegated administrator entry: account "${da.account}" + service principal "${da.servicePrincipal}".`
135
+ );
136
+ }
137
+ seen.add(key);
138
+ }
109
139
  }
110
140
  function checkInlinePolicySizes(config, errors) {
111
141
  for (const ps of config.permissionSets) {
package/dist/diff.js CHANGED
@@ -26,22 +26,26 @@ const operationExecutionPriority = {
26
26
  attachIdcCustomerManagedPolicyReferenceToPermissionSet: 20,
27
27
  detachIdcCustomerManagedPolicyReferenceFromPermissionSet: 21,
28
28
  provisionIdcPermissionSet: 22,
29
- grantIdcAccountAssignment: 23,
30
- removeIdcGroupMembership: 24,
31
- revokeIdcAccountAssignment: 25,
32
- deleteIdcUser: 26,
33
- deleteIdcGroup: 27,
34
- deleteIdcPermissionSet: 28,
35
- deleteOu: 29,
36
- createOrgPolicy: 30,
37
- updateOrgPolicyContent: 31,
38
- updateOrgPolicyDescription: 32,
39
- attachOrgPolicy: 33,
40
- detachOrgPolicy: 34,
41
- deleteOrgPolicy: 35,
42
- putAlternateContact: 36,
43
- deleteAlternateContact: 37,
44
- setIdcAccessControlAttributes: 38
29
+ putIdcPermissionSetPermissionsBoundary: 23,
30
+ deleteIdcPermissionSetPermissionsBoundary: 24,
31
+ grantIdcAccountAssignment: 25,
32
+ removeIdcGroupMembership: 26,
33
+ revokeIdcAccountAssignment: 27,
34
+ deleteIdcUser: 28,
35
+ deleteIdcGroup: 29,
36
+ deleteIdcPermissionSet: 30,
37
+ deleteOu: 31,
38
+ createOrgPolicy: 32,
39
+ updateOrgPolicyContent: 33,
40
+ updateOrgPolicyDescription: 34,
41
+ attachOrgPolicy: 35,
42
+ detachOrgPolicy: 36,
43
+ deleteOrgPolicy: 37,
44
+ putAlternateContact: 38,
45
+ deleteAlternateContact: 39,
46
+ setIdcAccessControlAttributes: 40,
47
+ registerDelegatedAdministrator: 41,
48
+ deregisterDelegatedAdministrator: 42
45
49
  };
46
50
  function diffStates(props) {
47
51
  const operations = [];
@@ -56,6 +60,11 @@ function diffStates(props) {
56
60
  const nextAccountIds = new Set(
57
61
  nextOrganization.accounts.filter((account) => account.id !== pendingCreationId).map((account) => account.id)
58
62
  );
63
+ const ouNamesBeingCreated = new Set(
64
+ nextOrganization.organizationalUnits.filter(
65
+ (ou) => !currentOrganization.organizationalUnitByName.has(ou.name)
66
+ ).map((ou) => ou.name)
67
+ );
59
68
  for (const nextAccount of nextOrganization.accounts) {
60
69
  const currentAccount = nextAccount.id !== pendingCreationId ? currentOrganization.accountById.get(nextAccount.id) : void 0;
61
70
  if (currentAccount == null) {
@@ -70,11 +79,21 @@ function diffStates(props) {
70
79
  organizationalUnitNameById: nextOrganization.organizationalUnitNameById,
71
80
  organizationalUnitId: nextAccount.parentId
72
81
  }) === false) {
73
- unsupported.push({
74
- kind: "newAccountWithUnknownOu",
75
- category: "unsupportedMutation",
76
- description: `new account "${nextAccount.name}" has unresolved target OU "${targetOuName}" (${nextAccount.parentId})`
77
- });
82
+ if (ouNamesBeingCreated.has(targetOuName)) {
83
+ operations.push({
84
+ kind: "createAccount",
85
+ accountName: nextAccount.name,
86
+ accountEmail: nextAccount.email,
87
+ targetOuId: nextAccount.parentId,
88
+ targetOuName
89
+ });
90
+ } else {
91
+ unsupported.push({
92
+ kind: "newAccountWithUnknownOu",
93
+ category: "unsupportedMutation",
94
+ description: `new account "${nextAccount.name}" has unresolved target OU "${targetOuName}" (${nextAccount.parentId})`
95
+ });
96
+ }
78
97
  continue;
79
98
  }
80
99
  operations.push({
@@ -117,7 +136,7 @@ function diffStates(props) {
117
136
  });
118
137
  continue;
119
138
  }
120
- if (currentAccount.id === pendingCreationId || nextAccount.id === pendingCreationId || currentAccount.parentId === pendingCreationId || nextAccount.parentId === pendingCreationId) {
139
+ if (currentAccount.id === pendingCreationId || nextAccount.id === pendingCreationId || currentAccount.parentId === pendingCreationId) {
121
140
  continue;
122
141
  }
123
142
  const fromOuName = resolveOrganizationalUnitName({
@@ -130,6 +149,41 @@ function diffStates(props) {
130
149
  rootId: nextOrganization.rootId,
131
150
  organizationalUnitId: nextAccount.parentId
132
151
  });
152
+ if (nextAccount.parentId === pendingCreationId) {
153
+ if (ouNamesBeingCreated.has(toOuName)) {
154
+ if (currentAccount.name !== nextAccount.name) {
155
+ operations.push({
156
+ kind: "updateAccountName",
157
+ accountId: nextAccount.id,
158
+ fromAccountName: currentAccount.name,
159
+ toAccountName: nextAccount.name
160
+ });
161
+ }
162
+ operations.push({
163
+ kind: "moveAccount",
164
+ accountId: nextAccount.id,
165
+ accountName: nextAccount.name,
166
+ fromOuId: currentAccount.parentId,
167
+ fromOuName,
168
+ toOuId: nextAccount.parentId,
169
+ toOuName
170
+ });
171
+ diffAlternateContacts({
172
+ operations,
173
+ accountId: nextAccount.id,
174
+ accountName: nextAccount.name,
175
+ currentContacts: currentAccount.alternateContacts ?? [],
176
+ nextContacts: nextAccount.alternateContacts ?? []
177
+ });
178
+ } else {
179
+ unsupported.push({
180
+ kind: "existingAccountWithUnknownTargetOu",
181
+ category: "unsupportedMutation",
182
+ description: `existing account "${nextAccount.name}" has unresolved target OU "${toOuName}" (${nextAccount.parentId})`
183
+ });
184
+ }
185
+ continue;
186
+ }
133
187
  if (currentAccount.name !== nextAccount.name) {
134
188
  operations.push({
135
189
  kind: "updateAccountName",
@@ -306,11 +360,20 @@ function diffStates(props) {
306
360
  organizationalUnitNameById: nextOrganization.organizationalUnitNameById,
307
361
  organizationalUnitId: addedOrganizationalUnit.parentId
308
362
  }) === false) {
309
- unsupported.push({
310
- kind: "newOuWithUnknownParent",
311
- category: "unsupportedMutation",
312
- description: `new OU "${addedOrganizationalUnit.name}" has unresolved parent "${parentOuName}" (${addedOrganizationalUnit.parentId})`
313
- });
363
+ if (ouNamesBeingCreated.has(parentOuName) && parentOuName !== addedOrganizationalUnit.name) {
364
+ operations.push({
365
+ kind: "createOu",
366
+ ouName: addedOrganizationalUnit.name,
367
+ parentOuId: addedOrganizationalUnit.parentId,
368
+ parentOuName
369
+ });
370
+ } else {
371
+ unsupported.push({
372
+ kind: "newOuWithUnknownParent",
373
+ category: "unsupportedMutation",
374
+ description: `new OU "${addedOrganizationalUnit.name}" has unresolved parent "${parentOuName}" (${addedOrganizationalUnit.parentId})`
375
+ });
376
+ }
314
377
  continue;
315
378
  }
316
379
  operations.push({
@@ -576,6 +639,23 @@ function diffStates(props) {
576
639
  customerManagedPolicyPath: customerManagedPolicy.path
577
640
  });
578
641
  }
642
+ const currentBoundary = currentPermissionSet?.permissionsBoundary ?? null;
643
+ const nextBoundary = nextPermissionSet.permissionsBoundary ?? null;
644
+ if (nextBoundary != null) {
645
+ if (currentBoundary == null || JSON.stringify(currentBoundary) !== JSON.stringify(nextBoundary)) {
646
+ operations.push({
647
+ kind: "putIdcPermissionSetPermissionsBoundary",
648
+ permissionSetName: nextPermissionSet.name,
649
+ permissionsBoundary: nextBoundary
650
+ });
651
+ }
652
+ }
653
+ if (nextBoundary == null && currentBoundary != null) {
654
+ operations.push({
655
+ kind: "deleteIdcPermissionSetPermissionsBoundary",
656
+ permissionSetName: nextPermissionSet.name
657
+ });
658
+ }
579
659
  if (currentPermissionSet != null && operations.length > permissionSetMutationStartIndex && permissionSetNamesWithDesiredAssignments.has(nextPermissionSet.name)) {
580
660
  operations.push({
581
661
  kind: "provisionIdcPermissionSet",
@@ -648,6 +728,54 @@ function diffStates(props) {
648
728
  attributes: nextAccessControlAttributes
649
729
  });
650
730
  }
731
+ const nextAccountNameById = new Map(
732
+ props.next.organization.accounts.map((account) => [
733
+ account.id,
734
+ account.name
735
+ ])
736
+ );
737
+ const currentAccountNameById = new Map(
738
+ props.current.organization.accounts.map((account) => [
739
+ account.id,
740
+ account.name
741
+ ])
742
+ );
743
+ const currentDelegatedAdmins = props.current.organization.delegatedAdministrators ?? [];
744
+ const nextDelegatedAdmins = props.next.organization.delegatedAdministrators ?? [];
745
+ const nextDelegatedAdminKeys = new Set(
746
+ nextDelegatedAdmins.map((da) => `${da.accountId}|${da.servicePrincipal}`)
747
+ );
748
+ const currentDelegatedAdminKeys = new Set(
749
+ currentDelegatedAdmins.map(
750
+ (da) => `${da.accountId}|${da.servicePrincipal}`
751
+ )
752
+ );
753
+ for (const nextDa of nextDelegatedAdmins) {
754
+ if (currentDelegatedAdminKeys.has(
755
+ `${nextDa.accountId}|${nextDa.servicePrincipal}`
756
+ )) {
757
+ continue;
758
+ }
759
+ operations.push({
760
+ kind: "registerDelegatedAdministrator",
761
+ accountId: nextDa.accountId,
762
+ accountName: nextAccountNameById.get(nextDa.accountId) ?? nextDa.accountId,
763
+ servicePrincipal: nextDa.servicePrincipal
764
+ });
765
+ }
766
+ for (const currentDa of currentDelegatedAdmins) {
767
+ if (nextDelegatedAdminKeys.has(
768
+ `${currentDa.accountId}|${currentDa.servicePrincipal}`
769
+ )) {
770
+ continue;
771
+ }
772
+ operations.push({
773
+ kind: "deregisterDelegatedAdministrator",
774
+ accountId: currentDa.accountId,
775
+ accountName: currentAccountNameById.get(currentDa.accountId) ?? currentDa.accountId,
776
+ servicePrincipal: currentDa.servicePrincipal
777
+ });
778
+ }
651
779
  const currentPolicies = props.current.organization.policies ?? [];
652
780
  const nextPolicies = props.next.organization.policies ?? [];
653
781
  const currentPolicyAttachments = props.current.organization.policyAttachments ?? [];
@@ -699,23 +827,22 @@ function diffStates(props) {
699
827
  const nextOuNameById = new Map(
700
828
  props.next.organization.organizationalUnits.map((ou) => [ou.id, ou.name])
701
829
  );
702
- const nextAccountNameById = new Map(
703
- props.next.organization.accounts.map((account) => [account.id, account.name])
704
- );
705
830
  const currentOuNameById = new Map(
706
- props.current.organization.organizationalUnits.map((ou) => [ou.id, ou.name])
707
- );
708
- const currentAccountNameById = new Map(
709
- props.current.organization.accounts.map((account) => [account.id, account.name])
831
+ props.current.organization.organizationalUnits.map((ou) => [
832
+ ou.id,
833
+ ou.name
834
+ ])
710
835
  );
711
836
  function resolveNextTargetName(targetId, targetType) {
712
837
  if (targetType === "ROOT") return "root";
713
- if (targetType === "ORGANIZATIONAL_UNIT") return nextOuNameById.get(targetId) ?? "unknown";
838
+ if (targetType === "ORGANIZATIONAL_UNIT")
839
+ return nextOuNameById.get(targetId) ?? "unknown";
714
840
  return nextAccountNameById.get(targetId) ?? "unknown";
715
841
  }
716
842
  function resolveCurrentTargetName(targetId, targetType) {
717
843
  if (targetType === "ROOT") return "root";
718
- if (targetType === "ORGANIZATIONAL_UNIT") return currentOuNameById.get(targetId) ?? "unknown";
844
+ if (targetType === "ORGANIZATIONAL_UNIT")
845
+ return currentOuNameById.get(targetId) ?? "unknown";
719
846
  return currentAccountNameById.get(targetId) ?? "unknown";
720
847
  }
721
848
  for (const nextAttachment of nextPolicyAttachments) {
@@ -796,6 +923,27 @@ function diffStates(props) {
796
923
  }
797
924
  return getOperationSortKey(left).localeCompare(getOperationSortKey(right));
798
925
  });
926
+ const createOuOps = operations.filter(
927
+ (op) => op.kind === "createOu"
928
+ );
929
+ const otherOps = operations.filter((op) => op.kind !== "createOu");
930
+ if (createOuOps.some((op) => op.parentOuId === pendingCreationId)) {
931
+ let visitOu2 = function(op) {
932
+ if (visited.has(op.ouName)) return;
933
+ visited.add(op.ouName);
934
+ if (op.parentOuId === pendingCreationId) {
935
+ const parent = createOuByName.get(op.parentOuName);
936
+ if (parent != null) visitOu2(parent);
937
+ }
938
+ topoSorted.push(op);
939
+ };
940
+ var visitOu = visitOu2;
941
+ const createOuByName = new Map(createOuOps.map((op) => [op.ouName, op]));
942
+ const visited = /* @__PURE__ */ new Set();
943
+ const topoSorted = [];
944
+ for (const op of createOuOps) visitOu2(op);
945
+ operations.splice(0, operations.length, ...topoSorted, ...otherOps);
946
+ }
799
947
  unsupported.sort((left, right) => {
800
948
  const kindComparison = left.kind.localeCompare(right.kind);
801
949
  if (kindComparison !== 0) {
@@ -1037,6 +1185,13 @@ function getOperationSortKey(operation) {
1037
1185
  if (operation.kind === "setIdcAccessControlAttributes") {
1038
1186
  return operation.kind;
1039
1187
  }
1188
+ if (operation.kind === "registerDelegatedAdministrator" || operation.kind === "deregisterDelegatedAdministrator") {
1189
+ return [
1190
+ operation.kind,
1191
+ operation.accountName,
1192
+ operation.servicePrincipal
1193
+ ].join("|");
1194
+ }
1040
1195
  return "zzzz";
1041
1196
  }
1042
1197
  function normalizeJsonContent(content) {