@beesolve/aws-accounts 1.0.7 → 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 +79 -10
- package/dist/applyLogic.js +52 -19
- package/dist/awsConfig.js +12 -9
- package/dist/cli.js +33 -0
- package/dist/commands/profile.js +116 -0
- package/dist/commands/remote.js +15 -11
- package/dist/commands/validate.js +80 -0
- package/dist/diff.js +24 -15
- package/dist/operations.js +8 -1
- package/dist/scanLogic.js +1 -0
- package/dist/state.js +2 -1
- package/dist-lambda/handler.mjs +63 -21
- package/dist-lambda/lambda.zip +0 -0
- package/package.json +1 -1
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
|
-
|
|
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
|
|
|
@@ -54,7 +59,9 @@ After `init`, `aws.config.ts` is your source of truth. Edit it to add accounts,
|
|
|
54
59
|
| `apply` | Executes planned operations via Lambda |
|
|
55
60
|
| `upgrade` | Updates the deployed Lambda function code |
|
|
56
61
|
| `scan` | Refreshes remote state in S3 (advanced/recovery use) |
|
|
62
|
+
| `validate` | Validates `aws.config.ts` locally without hitting AWS |
|
|
57
63
|
| `graveyard` | Lists accounts parked in the Graveyard OU |
|
|
64
|
+
| `profile` | Generates an AWS CLI SSO profile block from local state |
|
|
58
65
|
|
|
59
66
|
## Workflow
|
|
60
67
|
|
|
@@ -72,6 +79,20 @@ After `init`, your project contains:
|
|
|
72
79
|
- **`aws.config.ts`** — your desired state: OUs, accounts, users, groups, permission sets, assignments
|
|
73
80
|
- **`aws.config.types.ts`** — generated types and helpers for IDE autocomplete
|
|
74
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
|
+
|
|
75
96
|
### IAM Policy Helpers
|
|
76
97
|
|
|
77
98
|
`aws.config.types.ts` exports `iam` helpers with service-scoped action autocomplete:
|
|
@@ -102,10 +123,32 @@ When `init` generates your config, recognized IAM actions in inline policies are
|
|
|
102
123
|
- Update group descriptions
|
|
103
124
|
- Manage group memberships
|
|
104
125
|
- Create, update, and delete permission sets
|
|
126
|
+
- Set permission set session duration (ISO-8601, e.g. `"PT8H"` — default 1h, max 12h)
|
|
105
127
|
- Manage inline policies, AWS managed policies, and customer-managed policy references
|
|
106
128
|
- Grant and revoke account assignments
|
|
107
129
|
- Reprovision changed permission sets
|
|
108
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
|
+
|
|
109
152
|
## Plan/Apply Safety
|
|
110
153
|
|
|
111
154
|
- `plan` fetches current remote state from S3 before computing the diff.
|
|
@@ -140,20 +183,46 @@ npx aws-accounts plan # review remaining diff
|
|
|
140
183
|
npx aws-accounts apply # re-apply (add --allow-destructive if needed)
|
|
141
184
|
```
|
|
142
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
|
+
|
|
143
210
|
## CLI Options
|
|
144
211
|
|
|
145
212
|
```
|
|
146
213
|
npx aws-accounts <command> [options]
|
|
147
214
|
|
|
148
215
|
Options:
|
|
149
|
-
--profile <name>
|
|
150
|
-
--region <region>
|
|
151
|
-
--yes
|
|
152
|
-
--json
|
|
153
|
-
--allow-destructive
|
|
154
|
-
--ignore-unsupported
|
|
155
|
-
--refresh
|
|
156
|
-
--
|
|
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
|
|
157
226
|
```
|
|
158
227
|
|
|
159
228
|
## IAM Permissions
|
|
@@ -173,7 +242,7 @@ The CLI delegates all AWS operations to a deployed Lambda. Day-to-day usage requ
|
|
|
173
242
|
|
|
174
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).
|
|
175
244
|
|
|
176
|
-
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).
|
|
177
246
|
|
|
178
247
|
## FAQ
|
|
179
248
|
|
package/dist/applyLogic.js
CHANGED
|
@@ -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]) => ({
|
|
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(
|
|
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: [
|
|
963
|
-
(
|
|
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
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
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
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
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
|
@@ -89,6 +89,7 @@ const awsConfigModelSchema = v.strictObject({
|
|
|
89
89
|
v.strictObject({
|
|
90
90
|
name: v.string(),
|
|
91
91
|
description: v.string(),
|
|
92
|
+
sessionDuration: v.optional(v.string()),
|
|
92
93
|
inlinePolicy: v.optional(iamPolicyDocumentSchema),
|
|
93
94
|
awsManagedPolicies: v.array(v.string()),
|
|
94
95
|
customerManagedPolicies: v.array(
|
|
@@ -432,6 +433,7 @@ function mapStateToAwsConfig(props) {
|
|
|
432
433
|
(permissionSet) => ({
|
|
433
434
|
name: permissionSet.name,
|
|
434
435
|
description: permissionSet.description,
|
|
436
|
+
sessionDuration: permissionSet.sessionDuration ?? void 0,
|
|
435
437
|
inlinePolicy: permissionSet.inlinePolicy == null ? void 0 : parseInlinePolicyForConfig({
|
|
436
438
|
permissionSetName: permissionSet.name,
|
|
437
439
|
inlinePolicy: permissionSet.inlinePolicy
|
|
@@ -667,6 +669,7 @@ function mapAwsConfigToState(props) {
|
|
|
667
669
|
permissionSetArn: matchedPermissionSet?.permissionSetArn ?? pendingCreationId,
|
|
668
670
|
name: permissionSet.name,
|
|
669
671
|
description: permissionSet.description,
|
|
672
|
+
sessionDuration: permissionSet.sessionDuration ?? null,
|
|
670
673
|
inlinePolicy: stableStringifyInlinePolicy(permissionSet.inlinePolicy),
|
|
671
674
|
awsManagedPolicies: [...permissionSet.awsManagedPolicies],
|
|
672
675
|
customerManagedPolicies: permissionSet.customerManagedPolicies.map(
|
|
@@ -934,10 +937,14 @@ function renderPolicyActionString(value) {
|
|
|
934
937
|
if (knownActions == null || knownActions.includes(actionName) === false) {
|
|
935
938
|
return JSON.stringify(value);
|
|
936
939
|
}
|
|
937
|
-
|
|
938
|
-
|
|
940
|
+
const fnName = servicePrefixToCamelCase(servicePrefix);
|
|
941
|
+
if (isIdentifierSafeServicePrefix(fnName)) {
|
|
942
|
+
return `iam.${fnName}(${JSON.stringify(actionName)})`;
|
|
939
943
|
}
|
|
940
|
-
return `iam[${JSON.stringify(
|
|
944
|
+
return `iam[${JSON.stringify(fnName)}](${JSON.stringify(actionName)})`;
|
|
945
|
+
}
|
|
946
|
+
function servicePrefixToCamelCase(value) {
|
|
947
|
+
return value.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
941
948
|
}
|
|
942
949
|
function isIdentifierSafeServicePrefix(value) {
|
|
943
950
|
return /^[A-Za-z_$][A-Za-z0-9_$]*$/u.test(value);
|
|
@@ -974,9 +981,8 @@ function renderAwsConfigTypesTs(props) {
|
|
|
974
981
|
});
|
|
975
982
|
return `import * as v from "valibot";
|
|
976
983
|
import { iamPolicyDocumentSchema } from "@beesolve/iam-policy-ts";
|
|
984
|
+
export * as iam from "@beesolve/iam-policy-ts";
|
|
977
985
|
export {
|
|
978
|
-
iam,
|
|
979
|
-
iamAction,
|
|
980
986
|
iamActionCatalog,
|
|
981
987
|
iamActionCatalogActionCount,
|
|
982
988
|
iamActionCatalogSourceSha256,
|
|
@@ -992,10 +998,6 @@ export {
|
|
|
992
998
|
assertIamPolicyDocumentStrict,
|
|
993
999
|
} from "@beesolve/iam-policy-ts";
|
|
994
1000
|
export type {
|
|
995
|
-
IamActionCatalog,
|
|
996
|
-
IamPolicyServicePrefix,
|
|
997
|
-
IamPolicyActionNameByService,
|
|
998
|
-
IamPolicyActionForService,
|
|
999
1001
|
IamPolicyVersion,
|
|
1000
1002
|
IamPolicyScalar,
|
|
1001
1003
|
IamPolicyScalarList,
|
|
@@ -1055,6 +1057,7 @@ export const awsConfigSchema = v.strictObject({
|
|
|
1055
1057
|
v.strictObject({
|
|
1056
1058
|
name: v.string(),
|
|
1057
1059
|
description: v.string(),
|
|
1060
|
+
sessionDuration: v.optional(v.string()),
|
|
1058
1061
|
inlinePolicy: v.optional(iamPolicyDocumentSchema),
|
|
1059
1062
|
awsManagedPolicies: v.array(v.string()),
|
|
1060
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
|
+
};
|
package/dist/commands/remote.js
CHANGED
|
@@ -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:
|
|
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:
|
|
196
|
+
Action: organizations("*"),
|
|
197
197
|
Resource: "*"
|
|
198
198
|
},
|
|
199
199
|
{
|
|
200
200
|
Effect: "Allow",
|
|
201
|
-
Action: [
|
|
201
|
+
Action: [sso("*"), identitystore("*")],
|
|
202
202
|
Resource: "*"
|
|
203
203
|
},
|
|
204
204
|
{
|
|
205
205
|
Effect: "Allow",
|
|
206
|
-
Action: [
|
|
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
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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: [
|
|
223
|
+
Action: [account("PutAccountName")],
|
|
224
224
|
Resource: "*"
|
|
225
225
|
}
|
|
226
226
|
]
|
|
@@ -761,6 +761,10 @@ function formatOperationLine(operation) {
|
|
|
761
761
|
if (operation.kind === "revokeIdcAccountAssignment") {
|
|
762
762
|
return ` revoke IdC assignment "${operation.permissionSetName}" from ${formatPrincipalLabel(operation.principalType, operation.principalName)} on "${operation.accountName}"`;
|
|
763
763
|
}
|
|
764
|
+
if (operation.kind === "updateIdcPermissionSetSessionDuration") {
|
|
765
|
+
const duration = operation.sessionDuration ?? "default";
|
|
766
|
+
return ` update IdC permission set session duration "${operation.permissionSetName}" -> ${duration}`;
|
|
767
|
+
}
|
|
764
768
|
assertUnreachable(operation, "Unsupported operation kind in formatOperationLine.");
|
|
765
769
|
}
|
|
766
770
|
function formatPrincipalLabel(principalType, principalName) {
|
|
@@ -817,7 +821,7 @@ async function ensureOrganizationManagementPermissionSet(props) {
|
|
|
817
821
|
Version: "2012-10-17",
|
|
818
822
|
Statement: [{
|
|
819
823
|
Effect: "Allow",
|
|
820
|
-
Action: [
|
|
824
|
+
Action: [organizations("*"), sso("*"), identitystore("*"), account("*"), iam("*")],
|
|
821
825
|
Resource: "*"
|
|
822
826
|
}]
|
|
823
827
|
});
|
|
@@ -867,7 +871,7 @@ async function ensureOrganizationRemoteManagementPermissionSet(props) {
|
|
|
867
871
|
Version: "2012-10-17",
|
|
868
872
|
Statement: [{
|
|
869
873
|
Effect: "Allow",
|
|
870
|
-
Action: [
|
|
874
|
+
Action: [lambda("InvokeFunction")],
|
|
871
875
|
Resource: props.lambdaArn
|
|
872
876
|
}]
|
|
873
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
package/dist/operations.js
CHANGED
|
@@ -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(
|
package/dist-lambda/handler.mjs
CHANGED
|
@@ -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]) => ({
|
|
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(
|
|
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: [
|
|
2965
|
-
(
|
|
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
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
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
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
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
|
}
|
package/dist-lambda/lambda.zip
CHANGED
|
Binary file
|