@beesolve/aws-accounts 1.0.6 → 1.1.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 CHANGED
@@ -11,7 +11,12 @@ Config-driven management for AWS Organizations and IAM Identity Center. Define y
11
11
  npm install @beesolve/aws-accounts
12
12
  ```
13
13
 
14
- Requires Node.js 24+ and valid AWS credentials (via environment, profile, or SSO).
14
+ ## Prerequisites
15
+
16
+ - **Node.js 24+**
17
+ - **AWS Organization** with all features enabled
18
+ - **IAM Identity Center** enabled in the organization's management account (or delegated admin account)
19
+ - **AWS credentials** with access to the management account (via environment, profile, or SSO)
15
20
 
16
21
  ## Quick Start
17
22
 
@@ -22,21 +27,27 @@ npm init -y
22
27
  npm pkg set type=module
23
28
  npm install @beesolve/aws-accounts
24
29
 
25
- # 2. Deploy remote infrastructure (S3 bucket, IAM role, Lambda)
30
+ # 2. Initialize git and add a .gitignore
31
+ git init
32
+ echo -e "node_modules/\n.remote-state-cache.json" > .gitignore
33
+
34
+ # 3. Deploy remote infrastructure (S3 bucket, IAM role, Lambda)
26
35
  npx aws-accounts bootstrap --region us-east-1
27
36
 
28
- # 3. Scan your AWS org and generate aws.config.ts
37
+ # 4. Scan your AWS org and generate aws.config.ts
29
38
  npx aws-accounts init
30
39
 
31
- # 4. Edit aws.config.ts to model your desired state
40
+ # 5. Edit aws.config.ts to model your desired state
32
41
 
33
- # 5. Preview and apply changes
42
+ # 6. Preview and apply changes
34
43
  npx aws-accounts plan
35
44
  npx aws-accounts apply
36
45
  ```
37
46
 
38
47
  After `init`, `aws.config.ts` is your source of truth. Edit it to add accounts, move OUs, manage permission sets, and control access — then sync with `plan` / `apply`.
39
48
 
49
+ > **`.gitignore` recommendation:** Add `node_modules/` and `.remote-state-cache.json` to your `.gitignore`. The cache file is a local copy of remote state that varies per environment and should not be committed.
50
+
40
51
  ## Commands
41
52
 
42
53
  | Command | Description |
@@ -48,7 +59,9 @@ After `init`, `aws.config.ts` is your source of truth. Edit it to add accounts,
48
59
  | `apply` | Executes planned operations via Lambda |
49
60
  | `upgrade` | Updates the deployed Lambda function code |
50
61
  | `scan` | Refreshes remote state in S3 (advanced/recovery use) |
62
+ | `validate` | Validates `aws.config.ts` locally without hitting AWS |
51
63
  | `graveyard` | Lists accounts parked in the Graveyard OU |
64
+ | `profile` | Generates an AWS CLI SSO profile block from local state |
52
65
 
53
66
  ## Workflow
54
67
 
@@ -66,6 +79,20 @@ After `init`, your project contains:
66
79
  - **`aws.config.ts`** — your desired state: OUs, accounts, users, groups, permission sets, assignments
67
80
  - **`aws.config.types.ts`** — generated types and helpers for IDE autocomplete
68
81
 
82
+ ### Permission Sets
83
+
84
+ ```ts
85
+ permissionSets: [
86
+ {
87
+ name: "AdminAccess",
88
+ description: "Full administrator access",
89
+ sessionDuration: "PT8H", // ISO-8601 duration; omit to use the AWS default of 1h (max 12h)
90
+ awsManagedPolicies: ["arn:aws:iam::aws:policy/AdministratorAccess"],
91
+ customerManagedPolicies: [],
92
+ },
93
+ ],
94
+ ```
95
+
69
96
  ### IAM Policy Helpers
70
97
 
71
98
  `aws.config.types.ts` exports `iam` helpers with service-scoped action autocomplete:
@@ -96,10 +123,32 @@ When `init` generates your config, recognized IAM actions in inline policies are
96
123
  - Update group descriptions
97
124
  - Manage group memberships
98
125
  - Create, update, and delete permission sets
126
+ - Set permission set session duration (ISO-8601, e.g. `"PT8H"` — default 1h, max 12h)
99
127
  - Manage inline policies, AWS managed policies, and customer-managed policy references
100
128
  - Grant and revoke account assignments
101
129
  - Reprovision changed permission sets
102
130
 
131
+ ## Validating your config
132
+
133
+ Run `validate` before `plan` to catch mistakes locally without making any AWS API calls:
134
+
135
+ ```bash
136
+ npx aws-accounts validate
137
+ ```
138
+
139
+ It checks two layers:
140
+
141
+ **Schema and reference errors** — caught by compiling `aws.config.ts` against the generated types in `aws.config.types.ts`:
142
+ - Type mismatches and missing required fields
143
+ - References to unknown OUs, accounts, groups, users, or permission sets (enforced by the generated picklist types)
144
+
145
+ **Semantic errors** — additional checks run after the schema passes:
146
+ - Circular OU parent references (e.g. OU A has `parentName: "B"` and B has `parentName: "A"`)
147
+ - Assignments with no principal or with both `group` and `user` set
148
+ - Permission set inline policies exceeding the 10,240 character limit
149
+
150
+ Exits with code 1 if any errors are found, making it safe to use in CI before running `plan`.
151
+
103
152
  ## Plan/Apply Safety
104
153
 
105
154
  - `plan` fetches current remote state from S3 before computing the diff.
@@ -134,20 +183,46 @@ npx aws-accounts plan # review remaining diff
134
183
  npx aws-accounts apply # re-apply (add --allow-destructive if needed)
135
184
  ```
136
185
 
186
+ ## Generating AWS CLI profiles
187
+
188
+ The `profile` command reads your local state cache and presents an interactive picker of every account/permission-set combination you have access to, then prints a ready-to-paste `~/.aws/config` block:
189
+
190
+ ```bash
191
+ npx aws-accounts profile --sso-start-url https://d-xxxxxxxxxx.awsapps.com/start
192
+ ```
193
+
194
+ ```ini
195
+ [profile my-account-admin-access]
196
+ sso_session = sso
197
+ sso_account_id = 123456789012
198
+ sso_role_name = AdminAccess
199
+
200
+ [sso-session sso]
201
+ sso_start_url = https://d-xxxxxxxxxx.awsapps.com/start
202
+ sso_region = eu-central-1
203
+ sso_registration_scopes = sso:account:access
204
+ ```
205
+
206
+ The SSO start URL is not returned by the AWS API — set it via the flag or the `AWS_SSO_START_URL` environment variable to avoid typing it every time. Use `--sso-session <name>` to customise the session name (default: `sso`).
207
+
208
+ Requires a populated local state cache — run `plan` or `scan` first if the cache is empty.
209
+
137
210
  ## CLI Options
138
211
 
139
212
  ```
140
213
  npx aws-accounts <command> [options]
141
214
 
142
215
  Options:
143
- --profile <name> AWS profile (fallback: AWS_PROFILE)
144
- --region <region> AWS region (fallback: AWS_REGION, AWS_DEFAULT_REGION)
145
- --yes Skip interactive confirmations
146
- --json Output plan as JSON (plan command)
147
- --allow-destructive Allow destructive operations (apply command)
148
- --ignore-unsupported Proceed with non-destructive unsupported diffs (apply command)
149
- --refresh Force state refresh before planning (plan command)
150
- --help Show help
216
+ --profile <name> AWS profile (fallback: AWS_PROFILE)
217
+ --region <region> AWS region (fallback: AWS_REGION, AWS_DEFAULT_REGION)
218
+ --yes Skip interactive confirmations
219
+ --json Output plan as JSON (plan command)
220
+ --allow-destructive Allow destructive operations (apply command)
221
+ --ignore-unsupported Proceed with non-destructive unsupported diffs (apply command)
222
+ --refresh Force state refresh before planning (plan command)
223
+ --sso-start-url <url> IAM Identity Center access portal URL (fallback: AWS_SSO_START_URL)
224
+ --sso-session <name> SSO session name for profile output (default: sso)
225
+ --help Show help
151
226
  ```
152
227
 
153
228
  ## IAM Permissions
@@ -167,7 +242,7 @@ The CLI delegates all AWS operations to a deployed Lambda. Day-to-day usage requ
167
242
 
168
243
  `bootstrap` and `upgrade` require broader permissions for deploying infrastructure (S3, IAM, Lambda, SSO). See the full policy in the [docs](./docs/adr/002-architecture-and-technology-choices.md).
169
244
 
170
- Commands that need no AWS permissions: `regenerate` (local codegen only), `graveyard` (reads local cache only).
245
+ Commands that need no AWS permissions: `regenerate` (local codegen only), `validate` (local config checks only), `graveyard` (reads local cache only).
171
246
 
172
247
  ## FAQ
173
248
 
@@ -1,6 +1,4 @@
1
- import {
2
- PutAccountNameCommand
3
- } from "@aws-sdk/client-account";
1
+ import { PutAccountNameCommand } from "@aws-sdk/client-account";
4
2
  import {
5
3
  CreateOrganizationalUnitCommand,
6
4
  DeleteOrganizationalUnitCommand,
@@ -204,7 +202,10 @@ async function executeOperation(props) {
204
202
  workingState: props.state,
205
203
  account: {
206
204
  ...account,
207
- tags: Object.entries(operation.tags).map(([key, value]) => ({ key, value }))
205
+ tags: Object.entries(operation.tags).map(([key, value]) => ({
206
+ key,
207
+ value
208
+ }))
208
209
  }
209
210
  });
210
211
  }
@@ -246,7 +247,9 @@ async function executeOperation(props) {
246
247
  DestinationParentId: operation.toOuId
247
248
  })
248
249
  );
249
- props.logger.log(`Done: "${operation.accountName}" -> ${operation.toOuName}`);
250
+ props.logger.log(
251
+ `Done: "${operation.accountName}" -> ${operation.toOuName}`
252
+ );
250
253
  return moveAccountInWorkingState({
251
254
  workingState: props.state,
252
255
  accountId: operation.accountId,
@@ -473,7 +476,8 @@ async function executeOperation(props) {
473
476
  new CreatePermissionSetCommand({
474
477
  InstanceArn: props.state.identityCenter.instanceArn,
475
478
  Name: operation.permissionSetName,
476
- Description: operation.description.length > 0 ? operation.description : void 0
479
+ Description: operation.description.length > 0 ? operation.description : void 0,
480
+ SessionDuration: operation.sessionDuration ?? void 0
477
481
  })
478
482
  );
479
483
  const permissionSetArn = response.PermissionSet?.PermissionSetArn;
@@ -489,6 +493,7 @@ async function executeOperation(props) {
489
493
  permissionSetArn,
490
494
  name: operation.permissionSetName,
491
495
  description: operation.description,
496
+ sessionDuration: operation.sessionDuration,
492
497
  inlinePolicy: null,
493
498
  awsManagedPolicies: [],
494
499
  customerManagedPolicies: []
@@ -519,6 +524,30 @@ async function executeOperation(props) {
519
524
  }
520
525
  });
521
526
  }
527
+ if (operation.kind === "updateIdcPermissionSetSessionDuration") {
528
+ const permissionSet = resolvePermissionSetByName({
529
+ state: props.state,
530
+ permissionSetName: operation.permissionSetName
531
+ });
532
+ props.logger.log(
533
+ `Updating IdC permission set session duration for "${operation.permissionSetName}"...`
534
+ );
535
+ await props.ssoAdminClient.send(
536
+ new UpdatePermissionSetCommand({
537
+ InstanceArn: props.state.identityCenter.instanceArn,
538
+ PermissionSetArn: permissionSet.permissionSetArn,
539
+ SessionDuration: operation.sessionDuration ?? void 0
540
+ })
541
+ );
542
+ props.logger.log(`Done: "${operation.permissionSetName}"`);
543
+ return upsertIdcPermissionSetInWorkingState({
544
+ workingState: props.state,
545
+ permissionSet: {
546
+ ...permissionSet,
547
+ sessionDuration: operation.sessionDuration
548
+ }
549
+ });
550
+ }
522
551
  if (operation.kind === "deleteIdcPermissionSet") {
523
552
  const permissionSet = resolvePermissionSetByName({
524
553
  state: props.state,
@@ -959,9 +988,9 @@ function upsertPermissionSetPolicyState(props) {
959
988
  workingState: props.state,
960
989
  permissionSet: {
961
990
  ...nextPermissionSet,
962
- awsManagedPolicies: [...new Set(nextPermissionSet.awsManagedPolicies)].sort(
963
- (left, right) => left.localeCompare(right)
964
- ),
991
+ awsManagedPolicies: [
992
+ ...new Set(nextPermissionSet.awsManagedPolicies)
993
+ ].sort((left, right) => left.localeCompare(right)),
965
994
  customerManagedPolicies: [
966
995
  ...nextPermissionSet.customerManagedPolicies
967
996
  ].sort((left, right) => {
@@ -992,11 +1021,13 @@ async function assertOrganizationalUnitIsEmpty(props) {
992
1021
  });
993
1022
  if (childOrganizationalUnit != null) {
994
1023
  throw new Error(
995
- `Refusing to delete OU "${props.organizationalUnitName}": live AWS preflight failed [child-ou-present]: ${formatLivePreflightResource({
996
- resourceType: "child OU",
997
- name: childOrganizationalUnit.Name,
998
- id: childOrganizationalUnit.Id
999
- })} is still attached.`
1024
+ `Refusing to delete OU "${props.organizationalUnitName}": live AWS preflight failed [child-ou-present]: ${formatLivePreflightResource(
1025
+ {
1026
+ resourceType: "child OU",
1027
+ name: childOrganizationalUnit.Name,
1028
+ id: childOrganizationalUnit.Id
1029
+ }
1030
+ )} is still attached.`
1000
1031
  );
1001
1032
  }
1002
1033
  const account = await listFirstAccountForParent({
@@ -1005,11 +1036,13 @@ async function assertOrganizationalUnitIsEmpty(props) {
1005
1036
  });
1006
1037
  if (account != null) {
1007
1038
  throw new Error(
1008
- `Refusing to delete OU "${props.organizationalUnitName}": live AWS preflight failed [account-present]: ${formatLivePreflightResource({
1009
- resourceType: "account",
1010
- name: account.Name,
1011
- id: account.Id
1012
- })} is still attached.`
1039
+ `Refusing to delete OU "${props.organizationalUnitName}": live AWS preflight failed [account-present]: ${formatLivePreflightResource(
1040
+ {
1041
+ resourceType: "account",
1042
+ name: account.Name,
1043
+ id: account.Id
1044
+ }
1045
+ )} is still attached.`
1013
1046
  );
1014
1047
  }
1015
1048
  }
package/dist/awsConfig.js CHANGED
@@ -11,7 +11,6 @@ import {
11
11
  } from "@beesolve/iam-policy-ts";
12
12
  import {
13
13
  createAccessRoleName,
14
- readStateFile,
15
14
  validateState
16
15
  } from "./state.js";
17
16
  import { assertUnreachable, toRecordByProperty } from "./helpers.js";
@@ -90,6 +89,7 @@ const awsConfigModelSchema = v.strictObject({
90
89
  v.strictObject({
91
90
  name: v.string(),
92
91
  description: v.string(),
92
+ sessionDuration: v.optional(v.string()),
93
93
  inlinePolicy: v.optional(iamPolicyDocumentSchema),
94
94
  awsManagedPolicies: v.array(v.string()),
95
95
  customerManagedPolicies: v.array(
@@ -114,7 +114,7 @@ const moduleDirectoryPath = resolve(
114
114
  );
115
115
  const projectRootPath = resolve(moduleDirectoryPath, "..");
116
116
  async function writeAwsConfigFromState(props) {
117
- const state = await readStateFile(props.statePath);
117
+ const state = props.state;
118
118
  const context = await readAwsContextFile(props.contextPath);
119
119
  assertStateMatchesContext({
120
120
  state,
@@ -433,6 +433,7 @@ function mapStateToAwsConfig(props) {
433
433
  (permissionSet) => ({
434
434
  name: permissionSet.name,
435
435
  description: permissionSet.description,
436
+ sessionDuration: permissionSet.sessionDuration ?? void 0,
436
437
  inlinePolicy: permissionSet.inlinePolicy == null ? void 0 : parseInlinePolicyForConfig({
437
438
  permissionSetName: permissionSet.name,
438
439
  inlinePolicy: permissionSet.inlinePolicy
@@ -668,6 +669,7 @@ function mapAwsConfigToState(props) {
668
669
  permissionSetArn: matchedPermissionSet?.permissionSetArn ?? pendingCreationId,
669
670
  name: permissionSet.name,
670
671
  description: permissionSet.description,
672
+ sessionDuration: permissionSet.sessionDuration ?? null,
671
673
  inlinePolicy: stableStringifyInlinePolicy(permissionSet.inlinePolicy),
672
674
  awsManagedPolicies: [...permissionSet.awsManagedPolicies],
673
675
  customerManagedPolicies: permissionSet.customerManagedPolicies.map(
@@ -935,10 +937,14 @@ function renderPolicyActionString(value) {
935
937
  if (knownActions == null || knownActions.includes(actionName) === false) {
936
938
  return JSON.stringify(value);
937
939
  }
938
- if (isIdentifierSafeServicePrefix(servicePrefix)) {
939
- return `iam.${servicePrefix}(${JSON.stringify(actionName)})`;
940
+ const fnName = servicePrefixToCamelCase(servicePrefix);
941
+ if (isIdentifierSafeServicePrefix(fnName)) {
942
+ return `iam.${fnName}(${JSON.stringify(actionName)})`;
940
943
  }
941
- return `iam[${JSON.stringify(servicePrefix)}](${JSON.stringify(actionName)})`;
944
+ return `iam[${JSON.stringify(fnName)}](${JSON.stringify(actionName)})`;
945
+ }
946
+ function servicePrefixToCamelCase(value) {
947
+ return value.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
942
948
  }
943
949
  function isIdentifierSafeServicePrefix(value) {
944
950
  return /^[A-Za-z_$][A-Za-z0-9_$]*$/u.test(value);
@@ -975,9 +981,8 @@ function renderAwsConfigTypesTs(props) {
975
981
  });
976
982
  return `import * as v from "valibot";
977
983
  import { iamPolicyDocumentSchema } from "@beesolve/iam-policy-ts";
984
+ export * as iam from "@beesolve/iam-policy-ts";
978
985
  export {
979
- iam,
980
- iamAction,
981
986
  iamActionCatalog,
982
987
  iamActionCatalogActionCount,
983
988
  iamActionCatalogSourceSha256,
@@ -993,10 +998,6 @@ export {
993
998
  assertIamPolicyDocumentStrict,
994
999
  } from "@beesolve/iam-policy-ts";
995
1000
  export type {
996
- IamActionCatalog,
997
- IamPolicyServicePrefix,
998
- IamPolicyActionNameByService,
999
- IamPolicyActionForService,
1000
1001
  IamPolicyVersion,
1001
1002
  IamPolicyScalar,
1002
1003
  IamPolicyScalarList,
@@ -1056,6 +1057,7 @@ export const awsConfigSchema = v.strictObject({
1056
1057
  v.strictObject({
1057
1058
  name: v.string(),
1058
1059
  description: v.string(),
1060
+ sessionDuration: v.optional(v.string()),
1059
1061
  inlinePolicy: v.optional(iamPolicyDocumentSchema),
1060
1062
  awsManagedPolicies: v.array(v.string()),
1061
1063
  customerManagedPolicies: v.array(
package/dist/cli.js CHANGED
@@ -12,7 +12,9 @@ import {
12
12
  } from "./awsClientConfig.js";
13
13
  import { consoleLogger } from "./logger.js";
14
14
  import { runGraveyardCommand } from "./commands/graveyard.js";
15
+ import { runProfileCommand } from "./commands/profile.js";
15
16
  import { runRegenerateCommand } from "./commands/regenerate.js";
17
+ import { runValidateCommand } from "./commands/validate.js";
16
18
  import {
17
19
  runRemoteBootstrap,
18
20
  runRemoteScan,
@@ -31,7 +33,9 @@ const commands = [
31
33
  "scan",
32
34
  "init",
33
35
  "regenerate",
36
+ "validate",
34
37
  "graveyard",
38
+ "profile",
35
39
  "plan",
36
40
  "apply",
37
41
  "upgrade"
@@ -51,6 +55,8 @@ async function main() {
51
55
  "ignore-unsupported": { type: "boolean", default: false },
52
56
  "allow-destructive": { type: "boolean", default: false },
53
57
  refresh: { type: "boolean", default: false },
58
+ "sso-start-url": { type: "string" },
59
+ "sso-session": { type: "string", default: "sso" },
54
60
  help: { type: "boolean", default: false }
55
61
  },
56
62
  allowPositionals: true
@@ -87,6 +93,13 @@ async function main() {
87
93
  }
88
94
  return;
89
95
  }
96
+ if (command === "validate") {
97
+ const valid = await runValidateCommand({ logger });
98
+ if (!valid) {
99
+ process.exitCode = 1;
100
+ }
101
+ return;
102
+ }
90
103
  if (command === "graveyard") {
91
104
  await runGraveyardCommand({
92
105
  logger,
@@ -95,6 +108,22 @@ async function main() {
95
108
  });
96
109
  return;
97
110
  }
111
+ if (command === "profile") {
112
+ const ssoStartUrl = args.values["sso-start-url"] ?? process.env.AWS_SSO_START_URL;
113
+ if (ssoStartUrl == null) {
114
+ printHelp(logger);
115
+ throw toUsageError("--sso-start-url is required for the profile command (or set AWS_SSO_START_URL).");
116
+ }
117
+ await runProfileCommand({
118
+ logger,
119
+ cachePath: ".remote-state-cache.json",
120
+ contextPath,
121
+ ssoStartUrl,
122
+ ssoSession: args.values["sso-session"] ?? "sso",
123
+ isTty: process.stdin.isTTY
124
+ });
125
+ return;
126
+ }
98
127
  const overwriteConfirmation = buildOverwriteConfirmation({
99
128
  yes: args.values.yes ?? false,
100
129
  isTty: process.stdin.isTTY
@@ -152,7 +181,11 @@ function printHelp(logger) {
152
181
  " npm run cli -- init [--profile <name>] [--region <region>] [--yes]"
153
182
  );
154
183
  logger.log(" npm run cli -- regenerate [--yes]");
184
+ logger.log(" npm run cli -- validate");
155
185
  logger.log(" npm run cli -- graveyard");
186
+ logger.log(
187
+ " npm run cli -- profile --sso-start-url <url> [--sso-session <name>] (env: AWS_SSO_START_URL)"
188
+ );
156
189
  logger.log(
157
190
  " npm run cli -- plan [--profile <name>] [--region <region>] [--refresh]"
158
191
  );
@@ -0,0 +1,116 @@
1
+ import { createInterface } from "node:readline/promises";
2
+ import { readAwsContextFromFile } from "../awsConfig.js";
3
+ import { readStateCache } from "../remoteStateCache.js";
4
+ async function runProfileCommand(input) {
5
+ const cache = await readStateCache(input.cachePath);
6
+ if (cache == null) {
7
+ throw new Error(
8
+ `No remote state cache found at "${input.cachePath}". Run scan or plan first.`
9
+ );
10
+ }
11
+ const context = await readAwsContextFromFile(input.contextPath);
12
+ const region = context.deployment?.region || process.env.AWS_REGION || process.env.AWS_DEFAULT_REGION || "us-east-1";
13
+ const entries = buildProfileEntries(cache.state);
14
+ if (entries.length === 0) {
15
+ input.logger.log("No account assignments found in state cache.");
16
+ return;
17
+ }
18
+ const selected = await selectEntry({ entries, logger: input.logger, isTty: input.isTty });
19
+ if (selected == null) {
20
+ return;
21
+ }
22
+ const profileName = buildProfileName(selected);
23
+ const block = renderProfileBlock({
24
+ profileName,
25
+ ssoSession: input.ssoSession,
26
+ accountId: selected.accountId,
27
+ roleName: selected.permissionSetName,
28
+ ssoStartUrl: input.ssoStartUrl,
29
+ region,
30
+ ssoRegistrationScopes: "sso:account:access"
31
+ });
32
+ input.logger.log("");
33
+ input.logger.log(block);
34
+ }
35
+ async function selectEntry(props) {
36
+ if (props.isTty !== true) {
37
+ throw new Error(
38
+ "Profile command requires an interactive terminal. Use --account and --permission-set in non-interactive mode (not yet supported)."
39
+ );
40
+ }
41
+ props.logger.log("Select an account/permission-set combination:");
42
+ props.logger.log("");
43
+ for (const [index, entry] of props.entries.entries()) {
44
+ props.logger.log(
45
+ ` ${index + 1}. ${entry.accountName} / ${entry.permissionSetName} (${entry.accountId})`
46
+ );
47
+ }
48
+ props.logger.log("");
49
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
50
+ try {
51
+ let choice;
52
+ while (choice == null) {
53
+ const answer = await rl.question(`Enter number (1-${props.entries.length}): `);
54
+ const parsed = parseInt(answer.trim(), 10);
55
+ if (!Number.isNaN(parsed) && parsed >= 1 && parsed <= props.entries.length) {
56
+ choice = parsed;
57
+ } else {
58
+ props.logger.log(`Please enter a number between 1 and ${props.entries.length}.`);
59
+ }
60
+ }
61
+ return props.entries[choice - 1] ?? null;
62
+ } finally {
63
+ rl.close();
64
+ }
65
+ }
66
+ function buildProfileEntries(state) {
67
+ const accountById = Object.fromEntries(state.organization.accounts.map((a) => [a.id, a]));
68
+ const permissionSetByArn = Object.fromEntries(
69
+ state.identityCenter.permissionSets.map((ps) => [ps.permissionSetArn, ps])
70
+ );
71
+ const seen = /* @__PURE__ */ new Set();
72
+ const entries = [];
73
+ for (const assignment of state.identityCenter.accountAssignments) {
74
+ const key = `${assignment.accountId}|${assignment.permissionSetArn}`;
75
+ if (seen.has(key)) {
76
+ continue;
77
+ }
78
+ seen.add(key);
79
+ const account = accountById[assignment.accountId];
80
+ const permissionSet = permissionSetByArn[assignment.permissionSetArn];
81
+ if (account == null || permissionSet == null) {
82
+ continue;
83
+ }
84
+ entries.push({
85
+ accountId: account.id,
86
+ accountName: account.name,
87
+ permissionSetName: permissionSet.name
88
+ });
89
+ }
90
+ return entries.sort((a, b) => {
91
+ const accountCmp = a.accountName.localeCompare(b.accountName);
92
+ return accountCmp !== 0 ? accountCmp : a.permissionSetName.localeCompare(b.permissionSetName);
93
+ });
94
+ }
95
+ function buildProfileName(entry) {
96
+ return `${toKebabCase(entry.accountName)}-${toKebabCase(entry.permissionSetName)}`;
97
+ }
98
+ function toKebabCase(value) {
99
+ return value.replace(/([a-z])([A-Z])/g, "$1-$2").replace(/[\s_]+/g, "-").toLowerCase().replace(/[^a-z0-9-]/g, "");
100
+ }
101
+ function renderProfileBlock(props) {
102
+ return [
103
+ `[profile ${props.profileName}]`,
104
+ `sso_session = ${props.ssoSession}`,
105
+ `sso_account_id = ${props.accountId}`,
106
+ `sso_role_name = ${props.roleName}`,
107
+ ``,
108
+ `[sso-session ${props.ssoSession}]`,
109
+ `sso_start_url = ${props.ssoStartUrl}`,
110
+ `sso_region = ${props.region}`,
111
+ `sso_registration_scopes = ${props.ssoRegistrationScopes}`
112
+ ].join("\n");
113
+ }
114
+ export {
115
+ runProfileCommand
116
+ };
@@ -51,7 +51,7 @@ import {
51
51
  import { applyReservedOuDeletionGuard } from "../reservedOuDeletion.js";
52
52
  import { validateState } from "../state.js";
53
53
  import { assertUnreachable, delay } from "../helpers.js";
54
- import { iam } from "@beesolve/iam-policy-ts";
54
+ import { sts, organizations, sso, identitystore, s3, logs, account, iam, lambda } from "@beesolve/iam-policy-ts";
55
55
  const remoteCommandSchema = v.object({
56
56
  subcommand: v.picklist(["bootstrap", "scan", "init", "plan", "apply", "upgrade"]),
57
57
  profile: v.optional(v.string()),
@@ -179,7 +179,7 @@ async function ensureIamRole(props) {
179
179
  {
180
180
  Effect: "Allow",
181
181
  Principal: { Service: "lambda.amazonaws.com" },
182
- Action: iam.sts("AssumeRole")
182
+ Action: sts("AssumeRole")
183
183
  }
184
184
  ]
185
185
  });
@@ -193,17 +193,17 @@ async function ensureIamRole(props) {
193
193
  Statement: [
194
194
  {
195
195
  Effect: "Allow",
196
- Action: iam.organizations("*"),
196
+ Action: organizations("*"),
197
197
  Resource: "*"
198
198
  },
199
199
  {
200
200
  Effect: "Allow",
201
- Action: [iam.sso("*"), iam.identitystore("*")],
201
+ Action: [sso("*"), identitystore("*")],
202
202
  Resource: "*"
203
203
  },
204
204
  {
205
205
  Effect: "Allow",
206
- Action: [iam.s3("GetObject"), iam.s3("PutObject"), iam.s3("ListBucket")],
206
+ Action: [s3("GetObject"), s3("PutObject"), s3("ListBucket")],
207
207
  Resource: [
208
208
  `arn:aws:s3:::${props.bucketName}`,
209
209
  `arn:aws:s3:::${props.bucketName}/*`
@@ -212,15 +212,15 @@ async function ensureIamRole(props) {
212
212
  {
213
213
  Effect: "Allow",
214
214
  Action: [
215
- iam.logs("CreateLogGroup"),
216
- iam.logs("CreateLogStream"),
217
- iam.logs("PutLogEvents")
215
+ logs("CreateLogGroup"),
216
+ logs("CreateLogStream"),
217
+ logs("PutLogEvents")
218
218
  ],
219
219
  Resource: "arn:aws:logs:*:*:*"
220
220
  },
221
221
  {
222
222
  Effect: "Allow",
223
- Action: [iam.account("PutAccountName")],
223
+ Action: [account("PutAccountName")],
224
224
  Resource: "*"
225
225
  }
226
226
  ]
@@ -398,7 +398,6 @@ async function runRemoteScan(input) {
398
398
  await writeStateCache(cachePath, response.state);
399
399
  input.logger.log("State cache updated.");
400
400
  }
401
- const statePath = "state.json";
402
401
  async function runRemoteInit(input) {
403
402
  const deployment = await readDeploymentFromContext();
404
403
  input.logger.log("Invoking remote scan...");
@@ -421,12 +420,8 @@ async function runRemoteInit(input) {
421
420
  input.logger.log(` Groups: ${response.summary.groups}`);
422
421
  input.logger.log(` Permission Sets: ${response.summary.permissionSets}`);
423
422
  input.logger.log(` Account Assignments: ${response.summary.accountAssignments}`);
424
- await Promise.all([
425
- writeFile(statePath, `${JSON.stringify(response.state, null, 2)}
426
- `, "utf8"),
427
- writeStateCache(cachePath, response.state)
428
- ]);
429
- input.logger.log("State written to state.json and cache updated.");
423
+ await writeStateCache(cachePath, response.state);
424
+ input.logger.log("State cache updated.");
430
425
  const context = await readAwsContextFromFile(contextFilePath);
431
426
  const graveyardOu = response.state.organization.organizationalUnits.find(
432
427
  (ou) => ou.name === "Graveyard"
@@ -453,7 +448,7 @@ async function runRemoteInit(input) {
453
448
  await writeFile(contextFilePath, `${JSON.stringify(ordered, null, 2)}
454
449
  `, "utf8");
455
450
  const configWriteResult = await writeAwsConfigFromState({
456
- statePath,
451
+ state: response.state,
457
452
  contextPath: contextFilePath,
458
453
  configPath: configFilePath,
459
454
  typesPath: typesFilePath,
@@ -766,6 +761,10 @@ function formatOperationLine(operation) {
766
761
  if (operation.kind === "revokeIdcAccountAssignment") {
767
762
  return ` revoke IdC assignment "${operation.permissionSetName}" from ${formatPrincipalLabel(operation.principalType, operation.principalName)} on "${operation.accountName}"`;
768
763
  }
764
+ if (operation.kind === "updateIdcPermissionSetSessionDuration") {
765
+ const duration = operation.sessionDuration ?? "default";
766
+ return ` update IdC permission set session duration "${operation.permissionSetName}" -> ${duration}`;
767
+ }
769
768
  assertUnreachable(operation, "Unsupported operation kind in formatOperationLine.");
770
769
  }
771
770
  function formatPrincipalLabel(principalType, principalName) {
@@ -822,7 +821,7 @@ async function ensureOrganizationManagementPermissionSet(props) {
822
821
  Version: "2012-10-17",
823
822
  Statement: [{
824
823
  Effect: "Allow",
825
- Action: [iam.organizations("*"), iam.sso("*"), iam.identitystore("*"), iam.account("*"), iam.iam("*")],
824
+ Action: [organizations("*"), sso("*"), identitystore("*"), account("*"), iam("*")],
826
825
  Resource: "*"
827
826
  }]
828
827
  });
@@ -872,7 +871,7 @@ async function ensureOrganizationRemoteManagementPermissionSet(props) {
872
871
  Version: "2012-10-17",
873
872
  Statement: [{
874
873
  Effect: "Allow",
875
- Action: [iam.lambda("InvokeFunction")],
874
+ Action: [lambda("InvokeFunction")],
876
875
  Resource: props.lambdaArn
877
876
  }]
878
877
  });
@@ -0,0 +1,80 @@
1
+ import { loadAwsConfigModelFromTsFile } from "../awsConfig.js";
2
+ const INLINE_POLICY_MAX_CHARS = 10240;
3
+ async function runValidateCommand(input) {
4
+ const configPath = input.configPath ?? "aws.config.ts";
5
+ const typesPath = input.typesPath ?? "aws.config.types.ts";
6
+ let config;
7
+ try {
8
+ config = await loadAwsConfigModelFromTsFile({ configPath, typesPath });
9
+ } catch (error) {
10
+ input.logger.log(`Config error: ${error instanceof Error ? error.message : String(error)}`);
11
+ return false;
12
+ }
13
+ const errors = [];
14
+ checkCircularOuReferences(config, errors);
15
+ checkAssignmentPrincipals(config, errors);
16
+ checkInlinePolicySizes(config, errors);
17
+ if (errors.length > 0) {
18
+ for (const error of errors) {
19
+ input.logger.log(`Error: ${error}`);
20
+ }
21
+ input.logger.log(`
22
+ Validation failed with ${errors.length} error(s).`);
23
+ return false;
24
+ }
25
+ input.logger.log("Config is valid.");
26
+ return true;
27
+ }
28
+ function checkCircularOuReferences(config, errors) {
29
+ const parentByName = new Map(
30
+ config.organizationalUnits.map((ou) => [ou.name, ou.parentName])
31
+ );
32
+ const confirmed = /* @__PURE__ */ new Set();
33
+ for (const ou of config.organizationalUnits) {
34
+ if (ou.name === "root" || confirmed.has(ou.name)) {
35
+ continue;
36
+ }
37
+ const visited = /* @__PURE__ */ new Set();
38
+ let current = ou.name;
39
+ while (current != null) {
40
+ if (visited.has(current)) {
41
+ errors.push(`Circular OU reference detected: "${current}" is its own ancestor.`);
42
+ confirmed.add(current);
43
+ break;
44
+ }
45
+ visited.add(current);
46
+ current = parentByName.get(current) ?? null;
47
+ }
48
+ }
49
+ }
50
+ function checkAssignmentPrincipals(config, errors) {
51
+ for (const assignment of config.assignments) {
52
+ const hasGroup = assignment.group != null;
53
+ const hasUser = assignment.user != null;
54
+ if (hasGroup && hasUser) {
55
+ errors.push(
56
+ `Assignment for permission set "${assignment.permissionSet}" specifies both "group" and "user" \u2014 only one is allowed.`
57
+ );
58
+ } else if (!hasGroup && !hasUser) {
59
+ errors.push(
60
+ `Assignment for permission set "${assignment.permissionSet}" has no principal \u2014 "group" or "user" is required.`
61
+ );
62
+ }
63
+ }
64
+ }
65
+ function checkInlinePolicySizes(config, errors) {
66
+ for (const ps of config.permissionSets) {
67
+ if (ps.inlinePolicy == null) {
68
+ continue;
69
+ }
70
+ const length = JSON.stringify(ps.inlinePolicy).length;
71
+ if (length > INLINE_POLICY_MAX_CHARS) {
72
+ errors.push(
73
+ `Permission set "${ps.name}" inline policy is ${length} characters (limit: ${INLINE_POLICY_MAX_CHARS}).`
74
+ );
75
+ }
76
+ }
77
+ }
78
+ export {
79
+ runValidateCommand
80
+ };
package/dist/diff.js CHANGED
@@ -18,20 +18,21 @@ const operationExecutionPriority = {
18
18
  addIdcGroupMembership: 12,
19
19
  createIdcPermissionSet: 13,
20
20
  updateIdcPermissionSetDescription: 14,
21
- putIdcPermissionSetInlinePolicy: 15,
22
- deleteIdcPermissionSetInlinePolicy: 16,
23
- attachIdcManagedPolicyToPermissionSet: 17,
24
- detachIdcManagedPolicyFromPermissionSet: 18,
25
- attachIdcCustomerManagedPolicyReferenceToPermissionSet: 19,
26
- detachIdcCustomerManagedPolicyReferenceFromPermissionSet: 20,
27
- provisionIdcPermissionSet: 21,
28
- grantIdcAccountAssignment: 22,
29
- removeIdcGroupMembership: 23,
30
- revokeIdcAccountAssignment: 24,
31
- deleteIdcUser: 25,
32
- deleteIdcGroup: 26,
33
- deleteIdcPermissionSet: 27,
34
- deleteOu: 28
21
+ updateIdcPermissionSetSessionDuration: 15,
22
+ putIdcPermissionSetInlinePolicy: 16,
23
+ deleteIdcPermissionSetInlinePolicy: 17,
24
+ attachIdcManagedPolicyToPermissionSet: 18,
25
+ detachIdcManagedPolicyFromPermissionSet: 19,
26
+ attachIdcCustomerManagedPolicyReferenceToPermissionSet: 20,
27
+ detachIdcCustomerManagedPolicyReferenceFromPermissionSet: 21,
28
+ provisionIdcPermissionSet: 22,
29
+ grantIdcAccountAssignment: 23,
30
+ removeIdcGroupMembership: 24,
31
+ revokeIdcAccountAssignment: 25,
32
+ deleteIdcUser: 26,
33
+ deleteIdcGroup: 27,
34
+ deleteIdcPermissionSet: 28,
35
+ deleteOu: 29
35
36
  };
36
37
  function diffStates(props) {
37
38
  const operations = [];
@@ -440,7 +441,8 @@ function diffStates(props) {
440
441
  operations.push({
441
442
  kind: "createIdcPermissionSet",
442
443
  permissionSetName: nextPermissionSet.name,
443
- description: nextPermissionSet.description
444
+ description: nextPermissionSet.description,
445
+ sessionDuration: nextPermissionSet.sessionDuration
444
446
  });
445
447
  }
446
448
  const permissionSetMutationStartIndex = operations.length;
@@ -452,6 +454,13 @@ function diffStates(props) {
452
454
  description: nextPermissionSet.description
453
455
  });
454
456
  }
457
+ if (currentPermissionSet.sessionDuration !== nextPermissionSet.sessionDuration) {
458
+ operations.push({
459
+ kind: "updateIdcPermissionSetSessionDuration",
460
+ permissionSetName: nextPermissionSet.name,
461
+ sessionDuration: nextPermissionSet.sessionDuration
462
+ });
463
+ }
455
464
  }
456
465
  const currentInlinePolicy = normalizeInlinePolicyString(
457
466
  currentPermissionSet?.inlinePolicy ?? null
@@ -100,13 +100,19 @@ const removeIdcGroupMembershipOperationSchema = v.strictObject({
100
100
  const createIdcPermissionSetOperationSchema = v.strictObject({
101
101
  kind: v.literal("createIdcPermissionSet"),
102
102
  permissionSetName: v.string(),
103
- description: v.string()
103
+ description: v.string(),
104
+ sessionDuration: v.nullable(v.string())
104
105
  });
105
106
  const updateIdcPermissionSetDescriptionOperationSchema = v.strictObject({
106
107
  kind: v.literal("updateIdcPermissionSetDescription"),
107
108
  permissionSetName: v.string(),
108
109
  description: v.string()
109
110
  });
111
+ const updateIdcPermissionSetSessionDurationOperationSchema = v.strictObject({
112
+ kind: v.literal("updateIdcPermissionSetSessionDuration"),
113
+ permissionSetName: v.string(),
114
+ sessionDuration: v.nullable(v.string())
115
+ });
110
116
  const deleteIdcPermissionSetOperationSchema = v.strictObject({
111
117
  kind: v.literal("deleteIdcPermissionSet"),
112
118
  permissionSetName: v.string()
@@ -180,6 +186,7 @@ const operationSchema = v.variant("kind", [
180
186
  removeIdcGroupMembershipOperationSchema,
181
187
  createIdcPermissionSetOperationSchema,
182
188
  updateIdcPermissionSetDescriptionOperationSchema,
189
+ updateIdcPermissionSetSessionDurationOperationSchema,
183
190
  deleteIdcPermissionSetOperationSchema,
184
191
  putIdcPermissionSetInlinePolicyOperationSchema,
185
192
  deleteIdcPermissionSetInlinePolicyOperationSchema,
package/dist/scanLogic.js CHANGED
@@ -333,6 +333,7 @@ async function listPermissionSets(props) {
333
333
  permissionSetArn: permissionSet.PermissionSetArn,
334
334
  name: permissionSet.Name,
335
335
  description: permissionSet.Description ?? "",
336
+ sessionDuration: permissionSet.SessionDuration ?? null,
336
337
  inlinePolicy,
337
338
  awsManagedPolicies,
338
339
  customerManagedPolicies
package/dist/state.js CHANGED
@@ -46,6 +46,7 @@ const permissionSetSchema = v.strictObject({
46
46
  permissionSetArn: nonEmptyString,
47
47
  name: nonEmptyString,
48
48
  description: v.string(),
49
+ sessionDuration: v.nullable(v.string()),
49
50
  inlinePolicy: v.nullable(nonEmptyString),
50
51
  awsManagedPolicies: v.array(nonEmptyString),
51
52
  customerManagedPolicies: v.array(customerManagedPolicyReferenceSchema)
@@ -345,7 +346,7 @@ function removeIdcGroupFromWorkingState(props) {
345
346
  }
346
347
  function upsertIdcPermissionSetInWorkingState(props) {
347
348
  const currentPermissionSet = props.workingState.identityCenter.permissionSetsByName[props.permissionSet.name];
348
- if (currentPermissionSet != null && currentPermissionSet.permissionSetArn === props.permissionSet.permissionSetArn && currentPermissionSet.name === props.permissionSet.name && currentPermissionSet.description === props.permissionSet.description && currentPermissionSet.inlinePolicy === props.permissionSet.inlinePolicy && JSON.stringify(currentPermissionSet.awsManagedPolicies) === JSON.stringify(props.permissionSet.awsManagedPolicies) && JSON.stringify(currentPermissionSet.customerManagedPolicies) === JSON.stringify(props.permissionSet.customerManagedPolicies)) {
349
+ if (currentPermissionSet != null && currentPermissionSet.permissionSetArn === props.permissionSet.permissionSetArn && currentPermissionSet.name === props.permissionSet.name && currentPermissionSet.description === props.permissionSet.description && currentPermissionSet.sessionDuration === props.permissionSet.sessionDuration && currentPermissionSet.inlinePolicy === props.permissionSet.inlinePolicy && JSON.stringify(currentPermissionSet.awsManagedPolicies) === JSON.stringify(props.permissionSet.awsManagedPolicies) && JSON.stringify(currentPermissionSet.customerManagedPolicies) === JSON.stringify(props.permissionSet.customerManagedPolicies)) {
349
350
  return props.workingState;
350
351
  }
351
352
  const remainingPermissionSets = props.workingState.identityCenter.permissionSets.filter(
@@ -739,13 +739,19 @@ var removeIdcGroupMembershipOperationSchema = strictObject({
739
739
  var createIdcPermissionSetOperationSchema = strictObject({
740
740
  kind: literal("createIdcPermissionSet"),
741
741
  permissionSetName: string(),
742
- description: string()
742
+ description: string(),
743
+ sessionDuration: nullable(string())
743
744
  });
744
745
  var updateIdcPermissionSetDescriptionOperationSchema = strictObject({
745
746
  kind: literal("updateIdcPermissionSetDescription"),
746
747
  permissionSetName: string(),
747
748
  description: string()
748
749
  });
750
+ var updateIdcPermissionSetSessionDurationOperationSchema = strictObject({
751
+ kind: literal("updateIdcPermissionSetSessionDuration"),
752
+ permissionSetName: string(),
753
+ sessionDuration: nullable(string())
754
+ });
749
755
  var deleteIdcPermissionSetOperationSchema = strictObject({
750
756
  kind: literal("deleteIdcPermissionSet"),
751
757
  permissionSetName: string()
@@ -819,6 +825,7 @@ var operationSchema = variant("kind", [
819
825
  removeIdcGroupMembershipOperationSchema,
820
826
  createIdcPermissionSetOperationSchema,
821
827
  updateIdcPermissionSetDescriptionOperationSchema,
828
+ updateIdcPermissionSetSessionDurationOperationSchema,
822
829
  deleteIdcPermissionSetOperationSchema,
823
830
  putIdcPermissionSetInlinePolicyOperationSchema,
824
831
  deleteIdcPermissionSetInlinePolicyOperationSchema,
@@ -915,6 +922,7 @@ var permissionSetSchema = strictObject({
915
922
  permissionSetArn: nonEmptyString,
916
923
  name: nonEmptyString,
917
924
  description: string(),
925
+ sessionDuration: nullable(string()),
918
926
  inlinePolicy: nullable(nonEmptyString),
919
927
  awsManagedPolicies: array(nonEmptyString),
920
928
  customerManagedPolicies: array(customerManagedPolicyReferenceSchema)
@@ -1211,7 +1219,7 @@ function removeIdcGroupFromWorkingState(props) {
1211
1219
  }
1212
1220
  function upsertIdcPermissionSetInWorkingState(props) {
1213
1221
  const currentPermissionSet = props.workingState.identityCenter.permissionSetsByName[props.permissionSet.name];
1214
- if (currentPermissionSet != null && currentPermissionSet.permissionSetArn === props.permissionSet.permissionSetArn && currentPermissionSet.name === props.permissionSet.name && currentPermissionSet.description === props.permissionSet.description && currentPermissionSet.inlinePolicy === props.permissionSet.inlinePolicy && JSON.stringify(currentPermissionSet.awsManagedPolicies) === JSON.stringify(props.permissionSet.awsManagedPolicies) && JSON.stringify(currentPermissionSet.customerManagedPolicies) === JSON.stringify(props.permissionSet.customerManagedPolicies)) {
1222
+ if (currentPermissionSet != null && currentPermissionSet.permissionSetArn === props.permissionSet.permissionSetArn && currentPermissionSet.name === props.permissionSet.name && currentPermissionSet.description === props.permissionSet.description && currentPermissionSet.sessionDuration === props.permissionSet.sessionDuration && currentPermissionSet.inlinePolicy === props.permissionSet.inlinePolicy && JSON.stringify(currentPermissionSet.awsManagedPolicies) === JSON.stringify(props.permissionSet.awsManagedPolicies) && JSON.stringify(currentPermissionSet.customerManagedPolicies) === JSON.stringify(props.permissionSet.customerManagedPolicies)) {
1215
1223
  return props.workingState;
1216
1224
  }
1217
1225
  const remainingPermissionSets = props.workingState.identityCenter.permissionSets.filter(
@@ -1766,6 +1774,7 @@ async function listPermissionSets(props) {
1766
1774
  permissionSetArn: permissionSet.PermissionSetArn,
1767
1775
  name: permissionSet.Name,
1768
1776
  description: permissionSet.Description ?? "",
1777
+ sessionDuration: permissionSet.SessionDuration ?? null,
1769
1778
  inlinePolicy,
1770
1779
  awsManagedPolicies,
1771
1780
  customerManagedPolicies
@@ -1885,9 +1894,7 @@ async function listAccountsForPermissionSet(props) {
1885
1894
  }
1886
1895
 
1887
1896
  // src/applyLogic.ts
1888
- import {
1889
- PutAccountNameCommand
1890
- } from "@aws-sdk/client-account";
1897
+ import { PutAccountNameCommand } from "@aws-sdk/client-account";
1891
1898
  import {
1892
1899
  CreateOrganizationalUnitCommand,
1893
1900
  DeleteOrganizationalUnitCommand,
@@ -2206,7 +2213,10 @@ async function executeOperation(props) {
2206
2213
  workingState: props.state,
2207
2214
  account: {
2208
2215
  ...account,
2209
- tags: Object.entries(operation.tags).map(([key, value]) => ({ key, value }))
2216
+ tags: Object.entries(operation.tags).map(([key, value]) => ({
2217
+ key,
2218
+ value
2219
+ }))
2210
2220
  }
2211
2221
  });
2212
2222
  }
@@ -2248,7 +2258,9 @@ async function executeOperation(props) {
2248
2258
  DestinationParentId: operation.toOuId
2249
2259
  })
2250
2260
  );
2251
- props.logger.log(`Done: "${operation.accountName}" -> ${operation.toOuName}`);
2261
+ props.logger.log(
2262
+ `Done: "${operation.accountName}" -> ${operation.toOuName}`
2263
+ );
2252
2264
  return moveAccountInWorkingState({
2253
2265
  workingState: props.state,
2254
2266
  accountId: operation.accountId,
@@ -2475,7 +2487,8 @@ async function executeOperation(props) {
2475
2487
  new CreatePermissionSetCommand({
2476
2488
  InstanceArn: props.state.identityCenter.instanceArn,
2477
2489
  Name: operation.permissionSetName,
2478
- Description: operation.description.length > 0 ? operation.description : void 0
2490
+ Description: operation.description.length > 0 ? operation.description : void 0,
2491
+ SessionDuration: operation.sessionDuration ?? void 0
2479
2492
  })
2480
2493
  );
2481
2494
  const permissionSetArn = response.PermissionSet?.PermissionSetArn;
@@ -2491,6 +2504,7 @@ async function executeOperation(props) {
2491
2504
  permissionSetArn,
2492
2505
  name: operation.permissionSetName,
2493
2506
  description: operation.description,
2507
+ sessionDuration: operation.sessionDuration,
2494
2508
  inlinePolicy: null,
2495
2509
  awsManagedPolicies: [],
2496
2510
  customerManagedPolicies: []
@@ -2521,6 +2535,30 @@ async function executeOperation(props) {
2521
2535
  }
2522
2536
  });
2523
2537
  }
2538
+ if (operation.kind === "updateIdcPermissionSetSessionDuration") {
2539
+ const permissionSet = resolvePermissionSetByName({
2540
+ state: props.state,
2541
+ permissionSetName: operation.permissionSetName
2542
+ });
2543
+ props.logger.log(
2544
+ `Updating IdC permission set session duration for "${operation.permissionSetName}"...`
2545
+ );
2546
+ await props.ssoAdminClient.send(
2547
+ new UpdatePermissionSetCommand({
2548
+ InstanceArn: props.state.identityCenter.instanceArn,
2549
+ PermissionSetArn: permissionSet.permissionSetArn,
2550
+ SessionDuration: operation.sessionDuration ?? void 0
2551
+ })
2552
+ );
2553
+ props.logger.log(`Done: "${operation.permissionSetName}"`);
2554
+ return upsertIdcPermissionSetInWorkingState({
2555
+ workingState: props.state,
2556
+ permissionSet: {
2557
+ ...permissionSet,
2558
+ sessionDuration: operation.sessionDuration
2559
+ }
2560
+ });
2561
+ }
2524
2562
  if (operation.kind === "deleteIdcPermissionSet") {
2525
2563
  const permissionSet = resolvePermissionSetByName({
2526
2564
  state: props.state,
@@ -2961,9 +2999,9 @@ function upsertPermissionSetPolicyState(props) {
2961
2999
  workingState: props.state,
2962
3000
  permissionSet: {
2963
3001
  ...nextPermissionSet,
2964
- awsManagedPolicies: [...new Set(nextPermissionSet.awsManagedPolicies)].sort(
2965
- (left, right) => left.localeCompare(right)
2966
- ),
3002
+ awsManagedPolicies: [
3003
+ ...new Set(nextPermissionSet.awsManagedPolicies)
3004
+ ].sort((left, right) => left.localeCompare(right)),
2967
3005
  customerManagedPolicies: [
2968
3006
  ...nextPermissionSet.customerManagedPolicies
2969
3007
  ].sort((left, right) => {
@@ -2994,11 +3032,13 @@ async function assertOrganizationalUnitIsEmpty(props) {
2994
3032
  });
2995
3033
  if (childOrganizationalUnit != null) {
2996
3034
  throw new Error(
2997
- `Refusing to delete OU "${props.organizationalUnitName}": live AWS preflight failed [child-ou-present]: ${formatLivePreflightResource({
2998
- resourceType: "child OU",
2999
- name: childOrganizationalUnit.Name,
3000
- id: childOrganizationalUnit.Id
3001
- })} is still attached.`
3035
+ `Refusing to delete OU "${props.organizationalUnitName}": live AWS preflight failed [child-ou-present]: ${formatLivePreflightResource(
3036
+ {
3037
+ resourceType: "child OU",
3038
+ name: childOrganizationalUnit.Name,
3039
+ id: childOrganizationalUnit.Id
3040
+ }
3041
+ )} is still attached.`
3002
3042
  );
3003
3043
  }
3004
3044
  const account = await listFirstAccountForParent({
@@ -3007,11 +3047,13 @@ async function assertOrganizationalUnitIsEmpty(props) {
3007
3047
  });
3008
3048
  if (account != null) {
3009
3049
  throw new Error(
3010
- `Refusing to delete OU "${props.organizationalUnitName}": live AWS preflight failed [account-present]: ${formatLivePreflightResource({
3011
- resourceType: "account",
3012
- name: account.Name,
3013
- id: account.Id
3014
- })} is still attached.`
3050
+ `Refusing to delete OU "${props.organizationalUnitName}": live AWS preflight failed [account-present]: ${formatLivePreflightResource(
3051
+ {
3052
+ resourceType: "account",
3053
+ name: account.Name,
3054
+ id: account.Id
3055
+ }
3056
+ )} is still attached.`
3015
3057
  );
3016
3058
  }
3017
3059
  }
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@beesolve/aws-accounts",
3
- "version": "1.0.6",
3
+ "version": "1.1.0",
4
4
  "description": "AWS Organizations and IAM Identity Center management CLI",
5
5
  "main": "dist/cli.js",
6
6
  "bin": {