@beesolve/aws-accounts 1.0.7 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +80 -11
- package/dist/applyLogic.js +288 -19
- package/dist/awsConfig.js +414 -32
- package/dist/cli.js +95 -25
- package/dist/commands/graveyard.js +27 -0
- package/dist/commands/profile.js +116 -0
- package/dist/commands/remote.js +152 -47
- package/dist/commands/validate.js +125 -0
- package/dist/diff.js +278 -22
- package/dist/lambda/handler.js +8 -4
- package/dist/lambdaClient.js +5 -2
- package/dist/operations.js +91 -2
- package/dist/scanLogic.js +164 -7
- package/dist/state.js +164 -7
- package/dist-lambda/handler.mjs +707 -40
- package/dist-lambda/lambda.zip +0 -0
- package/package.json +1 -1
package/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
|
|
|
@@ -20,7 +25,7 @@ Requires Node.js 24+ and valid AWS credentials (via environment, profile, or SSO
|
|
|
20
25
|
mkdir my-org && cd my-org
|
|
21
26
|
npm init -y
|
|
22
27
|
npm pkg set type=module
|
|
23
|
-
npm install @beesolve/aws-accounts
|
|
28
|
+
npm install @beesolve/aws-accounts typescript
|
|
24
29
|
|
|
25
30
|
# 2. Initialize git and add a .gitignore
|
|
26
31
|
git init
|
|
@@ -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,15 +1,22 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
2
|
+
DeleteAlternateContactCommand,
|
|
3
|
+
PutAccountNameCommand,
|
|
4
|
+
PutAlternateContactCommand
|
|
3
5
|
} from "@aws-sdk/client-account";
|
|
4
6
|
import {
|
|
7
|
+
AttachPolicyCommand,
|
|
5
8
|
CreateOrganizationalUnitCommand,
|
|
9
|
+
CreatePolicyCommand,
|
|
6
10
|
DeleteOrganizationalUnitCommand,
|
|
11
|
+
DeletePolicyCommand,
|
|
12
|
+
DetachPolicyCommand,
|
|
7
13
|
ListAccountsForParentCommand,
|
|
8
14
|
ListOrganizationalUnitsForParentCommand,
|
|
9
15
|
MoveAccountCommand,
|
|
10
16
|
TagResourceCommand,
|
|
11
17
|
UntagResourceCommand,
|
|
12
|
-
UpdateOrganizationalUnitCommand
|
|
18
|
+
UpdateOrganizationalUnitCommand,
|
|
19
|
+
UpdatePolicyCommand
|
|
13
20
|
} from "@aws-sdk/client-organizations";
|
|
14
21
|
import {
|
|
15
22
|
CreateGroupMembershipCommand,
|
|
@@ -37,6 +44,7 @@ import {
|
|
|
37
44
|
DetachManagedPolicyFromPermissionSetCommand,
|
|
38
45
|
ProvisionPermissionSetCommand,
|
|
39
46
|
PutInlinePolicyToPermissionSetCommand,
|
|
47
|
+
UpdateInstanceAccessControlAttributeConfigurationCommand,
|
|
40
48
|
UpdatePermissionSetCommand
|
|
41
49
|
} from "@aws-sdk/client-sso-admin";
|
|
42
50
|
import { createAccountAndMoveToOu } from "./accountCreation.js";
|
|
@@ -44,6 +52,7 @@ import { assertUnreachable, delay } from "./helpers.js";
|
|
|
44
52
|
import {
|
|
45
53
|
addGroupMembershipToWorkingState,
|
|
46
54
|
addAccountAssignmentToWorkingState,
|
|
55
|
+
addOrgPolicyAttachmentToWorkingState,
|
|
47
56
|
createGroupMembershipKey,
|
|
48
57
|
moveAccountInWorkingState,
|
|
49
58
|
removeAccountAssignmentFromWorkingState,
|
|
@@ -52,12 +61,15 @@ import {
|
|
|
52
61
|
removeIdcPermissionSetFromWorkingState,
|
|
53
62
|
removeIdcUserFromWorkingState,
|
|
54
63
|
removeOrganizationalUnitFromWorkingState,
|
|
64
|
+
removeOrgPolicyAttachmentFromWorkingState,
|
|
65
|
+
removeOrgPolicyFromWorkingState,
|
|
55
66
|
renameOrganizationalUnitInWorkingState,
|
|
56
67
|
upsertIdcGroupInWorkingState,
|
|
57
68
|
upsertIdcPermissionSetInWorkingState,
|
|
58
69
|
upsertIdcUserInWorkingState,
|
|
59
70
|
upsertAccountInWorkingState,
|
|
60
|
-
upsertOrganizationalUnitInWorkingState
|
|
71
|
+
upsertOrganizationalUnitInWorkingState,
|
|
72
|
+
upsertOrgPolicyInWorkingState
|
|
61
73
|
} from "./state.js";
|
|
62
74
|
async function executeOperation(props) {
|
|
63
75
|
const operation = props.operation;
|
|
@@ -204,7 +216,10 @@ async function executeOperation(props) {
|
|
|
204
216
|
workingState: props.state,
|
|
205
217
|
account: {
|
|
206
218
|
...account,
|
|
207
|
-
tags: Object.entries(operation.tags).map(([key, value]) => ({
|
|
219
|
+
tags: Object.entries(operation.tags).map(([key, value]) => ({
|
|
220
|
+
key,
|
|
221
|
+
value
|
|
222
|
+
}))
|
|
208
223
|
}
|
|
209
224
|
});
|
|
210
225
|
}
|
|
@@ -246,7 +261,9 @@ async function executeOperation(props) {
|
|
|
246
261
|
DestinationParentId: operation.toOuId
|
|
247
262
|
})
|
|
248
263
|
);
|
|
249
|
-
props.logger.log(
|
|
264
|
+
props.logger.log(
|
|
265
|
+
`Done: "${operation.accountName}" -> ${operation.toOuName}`
|
|
266
|
+
);
|
|
250
267
|
return moveAccountInWorkingState({
|
|
251
268
|
workingState: props.state,
|
|
252
269
|
accountId: operation.accountId,
|
|
@@ -473,7 +490,8 @@ async function executeOperation(props) {
|
|
|
473
490
|
new CreatePermissionSetCommand({
|
|
474
491
|
InstanceArn: props.state.identityCenter.instanceArn,
|
|
475
492
|
Name: operation.permissionSetName,
|
|
476
|
-
Description: operation.description.length > 0 ? operation.description : void 0
|
|
493
|
+
Description: operation.description.length > 0 ? operation.description : void 0,
|
|
494
|
+
SessionDuration: operation.sessionDuration ?? void 0
|
|
477
495
|
})
|
|
478
496
|
);
|
|
479
497
|
const permissionSetArn = response.PermissionSet?.PermissionSetArn;
|
|
@@ -489,6 +507,7 @@ async function executeOperation(props) {
|
|
|
489
507
|
permissionSetArn,
|
|
490
508
|
name: operation.permissionSetName,
|
|
491
509
|
description: operation.description,
|
|
510
|
+
sessionDuration: operation.sessionDuration,
|
|
492
511
|
inlinePolicy: null,
|
|
493
512
|
awsManagedPolicies: [],
|
|
494
513
|
customerManagedPolicies: []
|
|
@@ -519,6 +538,30 @@ async function executeOperation(props) {
|
|
|
519
538
|
}
|
|
520
539
|
});
|
|
521
540
|
}
|
|
541
|
+
if (operation.kind === "updateIdcPermissionSetSessionDuration") {
|
|
542
|
+
const permissionSet = resolvePermissionSetByName({
|
|
543
|
+
state: props.state,
|
|
544
|
+
permissionSetName: operation.permissionSetName
|
|
545
|
+
});
|
|
546
|
+
props.logger.log(
|
|
547
|
+
`Updating IdC permission set session duration for "${operation.permissionSetName}"...`
|
|
548
|
+
);
|
|
549
|
+
await props.ssoAdminClient.send(
|
|
550
|
+
new UpdatePermissionSetCommand({
|
|
551
|
+
InstanceArn: props.state.identityCenter.instanceArn,
|
|
552
|
+
PermissionSetArn: permissionSet.permissionSetArn,
|
|
553
|
+
SessionDuration: operation.sessionDuration ?? void 0
|
|
554
|
+
})
|
|
555
|
+
);
|
|
556
|
+
props.logger.log(`Done: "${operation.permissionSetName}"`);
|
|
557
|
+
return upsertIdcPermissionSetInWorkingState({
|
|
558
|
+
workingState: props.state,
|
|
559
|
+
permissionSet: {
|
|
560
|
+
...permissionSet,
|
|
561
|
+
sessionDuration: operation.sessionDuration
|
|
562
|
+
}
|
|
563
|
+
});
|
|
564
|
+
}
|
|
522
565
|
if (operation.kind === "deleteIdcPermissionSet") {
|
|
523
566
|
const permissionSet = resolvePermissionSetByName({
|
|
524
567
|
state: props.state,
|
|
@@ -880,6 +923,218 @@ async function executeOperation(props) {
|
|
|
880
923
|
}
|
|
881
924
|
});
|
|
882
925
|
}
|
|
926
|
+
if (operation.kind === "createOrgPolicy") {
|
|
927
|
+
props.logger.log(
|
|
928
|
+
`Creating org policy "${operation.policyName}" (${operation.policyType})...`
|
|
929
|
+
);
|
|
930
|
+
const response = await props.organizationsClient.send(
|
|
931
|
+
new CreatePolicyCommand({
|
|
932
|
+
Name: operation.policyName,
|
|
933
|
+
Description: operation.description.length > 0 ? operation.description : void 0,
|
|
934
|
+
Content: operation.content,
|
|
935
|
+
Type: operation.policyType
|
|
936
|
+
})
|
|
937
|
+
);
|
|
938
|
+
const policy = response.Policy?.PolicySummary;
|
|
939
|
+
if (policy?.Id == null || policy.Arn == null) {
|
|
940
|
+
throw new Error(
|
|
941
|
+
`CreatePolicy for "${operation.policyName}" returned incomplete data.`
|
|
942
|
+
);
|
|
943
|
+
}
|
|
944
|
+
props.logger.log(`Done: "${operation.policyName}"`);
|
|
945
|
+
return upsertOrgPolicyInWorkingState({
|
|
946
|
+
workingState: props.state,
|
|
947
|
+
policy: {
|
|
948
|
+
id: policy.Id,
|
|
949
|
+
arn: policy.Arn,
|
|
950
|
+
name: operation.policyName,
|
|
951
|
+
description: operation.description,
|
|
952
|
+
type: operation.policyType,
|
|
953
|
+
content: operation.content
|
|
954
|
+
}
|
|
955
|
+
});
|
|
956
|
+
}
|
|
957
|
+
if (operation.kind === "updateOrgPolicyContent") {
|
|
958
|
+
props.logger.log(`Updating org policy content "${operation.policyName}"...`);
|
|
959
|
+
await props.organizationsClient.send(
|
|
960
|
+
new UpdatePolicyCommand({
|
|
961
|
+
PolicyId: operation.policyId,
|
|
962
|
+
Content: operation.content
|
|
963
|
+
})
|
|
964
|
+
);
|
|
965
|
+
props.logger.log(`Done: "${operation.policyName}"`);
|
|
966
|
+
const currentPolicy = props.state.organization.policiesById[operation.policyId];
|
|
967
|
+
if (currentPolicy == null) {
|
|
968
|
+
return props.state;
|
|
969
|
+
}
|
|
970
|
+
return upsertOrgPolicyInWorkingState({
|
|
971
|
+
workingState: props.state,
|
|
972
|
+
policy: { ...currentPolicy, content: operation.content }
|
|
973
|
+
});
|
|
974
|
+
}
|
|
975
|
+
if (operation.kind === "updateOrgPolicyDescription") {
|
|
976
|
+
props.logger.log(
|
|
977
|
+
`Updating org policy description "${operation.policyName}"...`
|
|
978
|
+
);
|
|
979
|
+
await props.organizationsClient.send(
|
|
980
|
+
new UpdatePolicyCommand({
|
|
981
|
+
PolicyId: operation.policyId,
|
|
982
|
+
Description: operation.description
|
|
983
|
+
})
|
|
984
|
+
);
|
|
985
|
+
props.logger.log(`Done: "${operation.policyName}"`);
|
|
986
|
+
const currentPolicy = props.state.organization.policiesById[operation.policyId];
|
|
987
|
+
if (currentPolicy == null) {
|
|
988
|
+
return props.state;
|
|
989
|
+
}
|
|
990
|
+
return upsertOrgPolicyInWorkingState({
|
|
991
|
+
workingState: props.state,
|
|
992
|
+
policy: { ...currentPolicy, description: operation.description }
|
|
993
|
+
});
|
|
994
|
+
}
|
|
995
|
+
if (operation.kind === "attachOrgPolicy") {
|
|
996
|
+
props.logger.log(
|
|
997
|
+
`Attaching org policy "${operation.policyName}" to "${operation.targetName}"...`
|
|
998
|
+
);
|
|
999
|
+
const resolvedPolicyId = resolvePolicyId({
|
|
1000
|
+
state: props.state,
|
|
1001
|
+
policyId: operation.policyId,
|
|
1002
|
+
policyName: operation.policyName
|
|
1003
|
+
});
|
|
1004
|
+
await props.organizationsClient.send(
|
|
1005
|
+
new AttachPolicyCommand({
|
|
1006
|
+
PolicyId: resolvedPolicyId,
|
|
1007
|
+
TargetId: operation.targetId
|
|
1008
|
+
})
|
|
1009
|
+
);
|
|
1010
|
+
props.logger.log(`Done: "${operation.policyName}" -> "${operation.targetName}"`);
|
|
1011
|
+
const targetType = operation.targetId === props.context.organization.rootId ? "ROOT" : props.state.organization.organizationalUnitsById[operation.targetId] != null ? "ORGANIZATIONAL_UNIT" : "ACCOUNT";
|
|
1012
|
+
return addOrgPolicyAttachmentToWorkingState({
|
|
1013
|
+
workingState: props.state,
|
|
1014
|
+
attachment: {
|
|
1015
|
+
policyId: resolvedPolicyId,
|
|
1016
|
+
targetId: operation.targetId,
|
|
1017
|
+
targetType
|
|
1018
|
+
}
|
|
1019
|
+
});
|
|
1020
|
+
}
|
|
1021
|
+
if (operation.kind === "detachOrgPolicy") {
|
|
1022
|
+
props.logger.log(
|
|
1023
|
+
`Detaching org policy "${operation.policyName}" from "${operation.targetName}"...`
|
|
1024
|
+
);
|
|
1025
|
+
await props.organizationsClient.send(
|
|
1026
|
+
new DetachPolicyCommand({
|
|
1027
|
+
PolicyId: operation.policyId,
|
|
1028
|
+
TargetId: operation.targetId
|
|
1029
|
+
})
|
|
1030
|
+
);
|
|
1031
|
+
props.logger.log(`Done: "${operation.policyName}" x "${operation.targetName}"`);
|
|
1032
|
+
return removeOrgPolicyAttachmentFromWorkingState({
|
|
1033
|
+
workingState: props.state,
|
|
1034
|
+
policyId: operation.policyId,
|
|
1035
|
+
targetId: operation.targetId
|
|
1036
|
+
});
|
|
1037
|
+
}
|
|
1038
|
+
if (operation.kind === "deleteOrgPolicy") {
|
|
1039
|
+
props.logger.log(`Deleting org policy "${operation.policyName}"...`);
|
|
1040
|
+
await props.organizationsClient.send(
|
|
1041
|
+
new DeletePolicyCommand({ PolicyId: operation.policyId })
|
|
1042
|
+
);
|
|
1043
|
+
props.logger.log(`Done: "${operation.policyName}"`);
|
|
1044
|
+
return removeOrgPolicyFromWorkingState({
|
|
1045
|
+
workingState: props.state,
|
|
1046
|
+
policyId: operation.policyId
|
|
1047
|
+
});
|
|
1048
|
+
}
|
|
1049
|
+
if (operation.kind === "putAlternateContact") {
|
|
1050
|
+
props.logger.log(
|
|
1051
|
+
`Setting ${operation.contactType} alternate contact for "${operation.accountName}" (${operation.accountId})...`
|
|
1052
|
+
);
|
|
1053
|
+
await props.accountClient.send(
|
|
1054
|
+
new PutAlternateContactCommand({
|
|
1055
|
+
AccountId: operation.accountId,
|
|
1056
|
+
AlternateContactType: operation.contactType,
|
|
1057
|
+
Name: operation.name,
|
|
1058
|
+
EmailAddress: operation.email,
|
|
1059
|
+
PhoneNumber: operation.phone,
|
|
1060
|
+
Title: operation.title
|
|
1061
|
+
})
|
|
1062
|
+
);
|
|
1063
|
+
props.logger.log(`Done: ${operation.contactType} contact for "${operation.accountName}"`);
|
|
1064
|
+
const account = props.state.organization.accountsById[operation.accountId];
|
|
1065
|
+
if (account == null) {
|
|
1066
|
+
throw new Error(
|
|
1067
|
+
`Could not resolve account (${operation.accountId}) in working state.`
|
|
1068
|
+
);
|
|
1069
|
+
}
|
|
1070
|
+
const updatedContacts = [
|
|
1071
|
+
...(account.alternateContacts ?? []).filter(
|
|
1072
|
+
(c) => c.contactType !== operation.contactType
|
|
1073
|
+
),
|
|
1074
|
+
{
|
|
1075
|
+
contactType: operation.contactType,
|
|
1076
|
+
name: operation.name,
|
|
1077
|
+
email: operation.email,
|
|
1078
|
+
phone: operation.phone,
|
|
1079
|
+
title: operation.title
|
|
1080
|
+
}
|
|
1081
|
+
];
|
|
1082
|
+
return upsertAccountInWorkingState({
|
|
1083
|
+
workingState: props.state,
|
|
1084
|
+
account: { ...account, alternateContacts: updatedContacts }
|
|
1085
|
+
});
|
|
1086
|
+
}
|
|
1087
|
+
if (operation.kind === "deleteAlternateContact") {
|
|
1088
|
+
props.logger.log(
|
|
1089
|
+
`Deleting ${operation.contactType} alternate contact for "${operation.accountName}" (${operation.accountId})...`
|
|
1090
|
+
);
|
|
1091
|
+
await props.accountClient.send(
|
|
1092
|
+
new DeleteAlternateContactCommand({
|
|
1093
|
+
AccountId: operation.accountId,
|
|
1094
|
+
AlternateContactType: operation.contactType
|
|
1095
|
+
})
|
|
1096
|
+
);
|
|
1097
|
+
props.logger.log(`Done: removed ${operation.contactType} contact for "${operation.accountName}"`);
|
|
1098
|
+
const account = props.state.organization.accountsById[operation.accountId];
|
|
1099
|
+
if (account == null) {
|
|
1100
|
+
throw new Error(
|
|
1101
|
+
`Could not resolve account (${operation.accountId}) in working state.`
|
|
1102
|
+
);
|
|
1103
|
+
}
|
|
1104
|
+
return upsertAccountInWorkingState({
|
|
1105
|
+
workingState: props.state,
|
|
1106
|
+
account: {
|
|
1107
|
+
...account,
|
|
1108
|
+
alternateContacts: (account.alternateContacts ?? []).filter(
|
|
1109
|
+
(c) => c.contactType !== operation.contactType
|
|
1110
|
+
)
|
|
1111
|
+
}
|
|
1112
|
+
});
|
|
1113
|
+
}
|
|
1114
|
+
if (operation.kind === "setIdcAccessControlAttributes") {
|
|
1115
|
+
props.logger.log(
|
|
1116
|
+
`Setting IdC access control attributes (${operation.attributes.length} attribute(s))...`
|
|
1117
|
+
);
|
|
1118
|
+
await props.ssoAdminClient.send(
|
|
1119
|
+
new UpdateInstanceAccessControlAttributeConfigurationCommand({
|
|
1120
|
+
InstanceArn: props.state.identityCenter.instanceArn,
|
|
1121
|
+
InstanceAccessControlAttributeConfiguration: {
|
|
1122
|
+
AccessControlAttributes: operation.attributes.map((attr) => ({
|
|
1123
|
+
Key: attr.key,
|
|
1124
|
+
Value: { Source: attr.source }
|
|
1125
|
+
}))
|
|
1126
|
+
}
|
|
1127
|
+
})
|
|
1128
|
+
);
|
|
1129
|
+
props.logger.log(`Done: access control attributes updated`);
|
|
1130
|
+
return {
|
|
1131
|
+
...props.state,
|
|
1132
|
+
identityCenter: {
|
|
1133
|
+
...props.state.identityCenter,
|
|
1134
|
+
accessControlAttributes: operation.attributes
|
|
1135
|
+
}
|
|
1136
|
+
};
|
|
1137
|
+
}
|
|
883
1138
|
assertUnreachable(operation, "Unsupported operation kind in apply.");
|
|
884
1139
|
}
|
|
885
1140
|
function resolveAssignmentDependencies(props) {
|
|
@@ -940,6 +1195,16 @@ function resolveGroupByDisplayName(props) {
|
|
|
940
1195
|
}
|
|
941
1196
|
return group;
|
|
942
1197
|
}
|
|
1198
|
+
function resolvePolicyId(props) {
|
|
1199
|
+
if (props.policyId !== "__pending_creation__") return props.policyId;
|
|
1200
|
+
const policy = props.state.organization.policiesByName[props.policyName];
|
|
1201
|
+
if (policy == null) {
|
|
1202
|
+
throw new Error(
|
|
1203
|
+
`Could not resolve policy "${props.policyName}" in working state.`
|
|
1204
|
+
);
|
|
1205
|
+
}
|
|
1206
|
+
return policy.id;
|
|
1207
|
+
}
|
|
943
1208
|
function resolvePermissionSetByName(props) {
|
|
944
1209
|
const permissionSet = props.state.identityCenter.permissionSetsByName[props.permissionSetName];
|
|
945
1210
|
if (permissionSet == null) {
|
|
@@ -959,9 +1224,9 @@ function upsertPermissionSetPolicyState(props) {
|
|
|
959
1224
|
workingState: props.state,
|
|
960
1225
|
permissionSet: {
|
|
961
1226
|
...nextPermissionSet,
|
|
962
|
-
awsManagedPolicies: [
|
|
963
|
-
(
|
|
964
|
-
),
|
|
1227
|
+
awsManagedPolicies: [
|
|
1228
|
+
...new Set(nextPermissionSet.awsManagedPolicies)
|
|
1229
|
+
].sort((left, right) => left.localeCompare(right)),
|
|
965
1230
|
customerManagedPolicies: [
|
|
966
1231
|
...nextPermissionSet.customerManagedPolicies
|
|
967
1232
|
].sort((left, right) => {
|
|
@@ -992,11 +1257,13 @@ async function assertOrganizationalUnitIsEmpty(props) {
|
|
|
992
1257
|
});
|
|
993
1258
|
if (childOrganizationalUnit != null) {
|
|
994
1259
|
throw new Error(
|
|
995
|
-
`Refusing to delete OU "${props.organizationalUnitName}": live AWS preflight failed [child-ou-present]: ${formatLivePreflightResource(
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1260
|
+
`Refusing to delete OU "${props.organizationalUnitName}": live AWS preflight failed [child-ou-present]: ${formatLivePreflightResource(
|
|
1261
|
+
{
|
|
1262
|
+
resourceType: "child OU",
|
|
1263
|
+
name: childOrganizationalUnit.Name,
|
|
1264
|
+
id: childOrganizationalUnit.Id
|
|
1265
|
+
}
|
|
1266
|
+
)} is still attached.`
|
|
1000
1267
|
);
|
|
1001
1268
|
}
|
|
1002
1269
|
const account = await listFirstAccountForParent({
|
|
@@ -1005,11 +1272,13 @@ async function assertOrganizationalUnitIsEmpty(props) {
|
|
|
1005
1272
|
});
|
|
1006
1273
|
if (account != null) {
|
|
1007
1274
|
throw new Error(
|
|
1008
|
-
`Refusing to delete OU "${props.organizationalUnitName}": live AWS preflight failed [account-present]: ${formatLivePreflightResource(
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1275
|
+
`Refusing to delete OU "${props.organizationalUnitName}": live AWS preflight failed [account-present]: ${formatLivePreflightResource(
|
|
1276
|
+
{
|
|
1277
|
+
resourceType: "account",
|
|
1278
|
+
name: account.Name,
|
|
1279
|
+
id: account.Id
|
|
1280
|
+
}
|
|
1281
|
+
)} is still attached.`
|
|
1013
1282
|
);
|
|
1014
1283
|
}
|
|
1015
1284
|
}
|