| 1 | import * as organizations from "@distilled.cloud/aws/organizations"; |
| 2 | import type * as Duration from "effect/Duration"; |
| 3 | import * as Effect from "effect/Effect"; |
| 4 | import type { ServiceControlPolicyDocument } from "../IAM/Policy.ts"; |
| 5 | import * as IdentityCenter from "../IdentityCenter/index.ts"; |
| 6 | import type { Account, AccountProps } from "./Account.ts"; |
| 7 | import { Account as OrganizationAccount } from "./Account.ts"; |
| 8 | import type { DelegatedAdministrator } from "./DelegatedAdministrator.ts"; |
| 9 | import { DelegatedAdministrator as OrganizationsDelegatedAdministrator } from "./DelegatedAdministrator.ts"; |
| 10 | import type { Organization, OrganizationProps } from "./Organization.ts"; |
| 11 | import { Organization as AwsOrganization } from "./Organization.ts"; |
| 12 | import type { |
| 13 | OrganizationalUnit, |
| 14 | OrganizationalUnitProps, |
| 15 | } from "./OrganizationalUnit.ts"; |
| 16 | import { OrganizationalUnit as AwsOrganizationalUnit } from "./OrganizationalUnit.ts"; |
| 17 | import type { Policy } from "./Policy.ts"; |
| 18 | import { Policy as OrganizationsPolicy } from "./Policy.ts"; |
| 19 | import type { PolicyAttachment } from "./PolicyAttachment.ts"; |
| 20 | import { PolicyAttachment as OrganizationsPolicyAttachment } from "./PolicyAttachment.ts"; |
| 21 | import type { Root, RootProps } from "./Root.ts"; |
| 22 | import { Root as OrganizationRoot } from "./Root.ts"; |
| 23 | import type { RootPolicyType } from "./RootPolicyType.ts"; |
| 24 | import { RootPolicyType as OrganizationsRootPolicyType } from "./RootPolicyType.ts"; |
| 25 | import type { TrustedServiceAccess } from "./TrustedServiceAccess.ts"; |
| 26 | import { TrustedServiceAccess as OrganizationsTrustedServiceAccess } from "./TrustedServiceAccess.ts"; |
| 27 | |
| 28 | export type TenantTargetKey = "root" | string; |
| 29 | |
| 30 | export interface TenantAccountSpec extends Omit< |
| 31 | AccountProps, |
| 32 | "parentId" | "name" | "email" |
| 33 | > { |
| 34 | /** Stable key identifying the account within the tenant (used in logical IDs and `targetKeys`). */ |
| 35 | key: string; |
| 36 | /** Friendly account name. */ |
| 37 | name: string; |
| 38 | /** Globally unique email address for the account. */ |
| 39 | email: string; |
| 40 | } |
| 41 | |
| 42 | export interface TenantOrganizationalUnitSpec extends Omit< |
| 43 | OrganizationalUnitProps, |
| 44 | "parentId" | "name" |
| 45 | > { |
| 46 | /** Stable key identifying the OU within the tenant (used in logical IDs and `targetKeys`). */ |
| 47 | key: string; |
| 48 | /** |
| 49 | * OU name. |
| 50 | * @default the spec `key` |
| 51 | */ |
| 52 | name?: string; |
| 53 | /** Member accounts vended directly under this OU. */ |
| 54 | accounts?: TenantAccountSpec[]; |
| 55 | /** Nested child OUs. */ |
| 56 | children?: TenantOrganizationalUnitSpec[]; |
| 57 | } |
| 58 | |
| 59 | export interface TenantPolicySpec { |
| 60 | /** Stable key identifying the policy within the tenant. */ |
| 61 | key: string; |
| 62 | /** Policy name. If omitted, Alchemy generates one. */ |
| 63 | name?: string; |
| 64 | /** Policy description. */ |
| 65 | description?: string; |
| 66 | /** |
| 67 | * Organizations policy type. |
| 68 | * @default "SERVICE_CONTROL_POLICY" |
| 69 | */ |
| 70 | type?: organizations.PolicyType; |
| 71 | /** |
| 72 | * Policy content — a typed {@link ServiceControlPolicyDocument} for |
| 73 | * SCP/RCP policies, or a raw JSON `string` for other policy types |
| 74 | * (tag, backup, ...) and as the escape hatch. |
| 75 | */ |
| 76 | document: ServiceControlPolicyDocument | string; |
| 77 | /** Keys of the roots/OUs/accounts to attach the policy to (`"root"` or a spec key). */ |
| 78 | targetKeys: TenantTargetKey[]; |
| 79 | /** Tags applied to the policy, merged with tenant-wide tags. */ |
| 80 | tags?: Record<string, string>; |
| 81 | } |
| 82 | |
| 83 | export interface TenantIdentityCenterGroupSpec { |
| 84 | /** Stable key identifying the group within the tenant. */ |
| 85 | key: string; |
| 86 | /** Display name of the Identity Center group. */ |
| 87 | displayName: string; |
| 88 | /** Group description. */ |
| 89 | description?: string; |
| 90 | } |
| 91 | |
| 92 | export interface TenantIdentityCenterPermissionSetSpec { |
| 93 | /** Stable key identifying the permission set within the tenant. */ |
| 94 | key: string; |
| 95 | /** Name of the permission set. */ |
| 96 | name: string; |
| 97 | /** Permission set description. */ |
| 98 | description?: string; |
| 99 | /** |
| 100 | * Optional session duration, e.g. `"8 hours"` or `Duration.hours(8)`. |
| 101 | * Sent to Identity Center as an ISO-8601 string such as `PT8H` (a bare |
| 102 | * number is milliseconds). |
| 103 | */ |
| 104 | sessionDuration?: Duration.Input; |
| 105 | /** URL the user lands on after federating into the account. */ |
| 106 | relayState?: string; |
| 107 | } |
| 108 | |
| 109 | export interface TenantIdentityCenterAssignmentSpec { |
| 110 | /** Stable key for the assignment. Defaults to a key derived from the other fields. */ |
| 111 | key?: string; |
| 112 | /** Key of the permission set to assign. */ |
| 113 | permissionSetKey: string; |
| 114 | /** Key of the account the principal is granted access to. */ |
| 115 | accountKey: string; |
| 116 | /** |
| 117 | * Kind of principal being assigned. |
| 118 | * @default "GROUP" |
| 119 | */ |
| 120 | principalType?: "USER" | "GROUP"; |
| 121 | /** Key of the Identity Center group to assign (when `principalType` is `GROUP`). */ |
| 122 | groupKey?: string; |
| 123 | /** Explicit principal ID (e.g. an existing user) instead of a group key. */ |
| 124 | principalId?: string; |
| 125 | } |
| 126 | |
| 127 | export interface TenantIdentityCenterSpec { |
| 128 | /** |
| 129 | * Whether to adopt the org's existing Identity Center instance |
| 130 | * (`existing`) or create an account instance (`account`). |
| 131 | * @default "existing" |
| 132 | */ |
| 133 | mode?: "existing" | "account"; |
| 134 | /** ARN of an existing Identity Center instance to use explicitly. */ |
| 135 | instanceArn?: string; |
| 136 | /** Name for the Identity Center instance. */ |
| 137 | name?: string; |
| 138 | /** Account key to register as the Identity Center delegated administrator. */ |
| 139 | delegatedAdminAccountKey?: string; |
| 140 | /** Identity Center groups to create. */ |
| 141 | groups?: TenantIdentityCenterGroupSpec[]; |
| 142 | /** Permission sets to create. */ |
| 143 | permissionSets?: TenantIdentityCenterPermissionSetSpec[]; |
| 144 | /** Account assignments binding groups/users to permission sets. */ |
| 145 | assignments?: TenantIdentityCenterAssignmentSpec[]; |
| 146 | } |
| 147 | |
| 148 | export interface TenantRootProps { |
| 149 | /** Organization settings (feature set). Defaults to `featureSet: "ALL"`. */ |
| 150 | organization?: OrganizationProps; |
| 151 | /** Root import settings (explicit root ID, tags). */ |
| 152 | root?: RootProps; |
| 153 | /** |
| 154 | * Policy types to enable on the root. |
| 155 | * @default ["SERVICE_CONTROL_POLICY"] |
| 156 | */ |
| 157 | policyTypes?: organizations.PolicyType[]; |
| 158 | /** Service principals granted trusted access (Identity Center's is added automatically). */ |
| 159 | trustedServicePrincipals?: string[]; |
| 160 | /** OU tree to create under the root. Defaults to a security/infrastructure/workloads baseline. */ |
| 161 | organizationalUnits?: TenantOrganizationalUnitSpec[]; |
| 162 | /** Organizations policies to create and attach via `targetKeys`. */ |
| 163 | policies?: TenantPolicySpec[]; |
| 164 | /** IAM Identity Center groups, permission sets, and assignments. */ |
| 165 | identityCenter?: TenantIdentityCenterSpec; |
| 166 | /** Tags applied to every taggable resource in the tenant. */ |
| 167 | tags?: Record<string, string>; |
| 168 | } |
| 169 | |
| 170 | export interface TenantRootResult { |
| 171 | organization: Organization; |
| 172 | root: Root; |
| 173 | policyTypes: RootPolicyType[]; |
| 174 | trustedServiceAccess: TrustedServiceAccess[]; |
| 175 | delegatedAdministrators: DelegatedAdministrator[]; |
| 176 | organizationalUnits: Record<string, OrganizationalUnit>; |
| 177 | accounts: Record<string, Account>; |
| 178 | policies: Record<string, Policy>; |
| 179 | policyAttachments: PolicyAttachment[]; |
| 180 | identityCenter?: { |
| 181 | instance: IdentityCenter.Instance; |
| 182 | groups: Record<string, IdentityCenter.Group>; |
| 183 | permissionSets: Record<string, IdentityCenter.PermissionSet>; |
| 184 | assignments: Record<string, IdentityCenter.AccountAssignment>; |
| 185 | }; |
| 186 | } |
| 187 | |
| 188 | const defaultTenantOrganizationalUnits = (): TenantOrganizationalUnitSpec[] => [ |
| 189 | { |
| 190 | key: "security", |
| 191 | name: "security", |
| 192 | accounts: [ |
| 193 | { |
| 194 | key: "security", |
| 195 | name: "security", |
| 196 | email: "security@example.com", |
| 197 | }, |
| 198 | { |
| 199 | key: "log-archive", |
| 200 | name: "log-archive", |
| 201 | email: "log-archive@example.com", |
| 202 | }, |
| 203 | ], |
| 204 | }, |
| 205 | { |
| 206 | key: "infrastructure", |
| 207 | name: "infrastructure", |
| 208 | accounts: [ |
| 209 | { |
| 210 | key: "shared-services", |
| 211 | name: "shared-services", |
| 212 | email: "shared-services@example.com", |
| 213 | }, |
| 214 | ], |
| 215 | }, |
| 216 | { |
| 217 | key: "workloads", |
| 218 | name: "workloads", |
| 219 | accounts: [ |
| 220 | { |
| 221 | key: "prod", |
| 222 | name: "prod", |
| 223 | email: "prod@example.com", |
| 224 | }, |
| 225 | ], |
| 226 | }, |
| 227 | ]; |
| 228 | |
| 229 | const toLogicalIdSegment = (value: string) => |
| 230 | value |
| 231 | .split(/[^A-Za-z0-9]+/) |
| 232 | .filter(Boolean) |
| 233 | .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) |
| 234 | .join(""); |
| 235 | |
| 236 | /** |
| 237 | * Compose an opinionated single-tenant landing zone inside the current AWS |
| 238 | * Organizations management account. |
| 239 | * |
| 240 | * This helper intentionally stays aligned to native AWS semantics: |
| 241 | * one real Organization, one root, nested OUs, and accounts beneath that |
| 242 | * tenant root. The broader `RootRoot` concept is an Alchemy control-plane |
| 243 | * abstraction over many such tenant roots deployed into separate management |
| 244 | * accounts, not a nested AWS Organizations feature. |
| 245 | * ### Creating A Tenant Root |
| 246 | * **Example:** Tenant With Baseline Accounts |
| 247 | * ```typescript |
| 248 | * const tenant = yield* TenantRoot("CustomerA", { |
| 249 | * identityCenter: { |
| 250 | * mode: "existing", |
| 251 | * groups: [ |
| 252 | * { key: "platform", displayName: "platform-engineers" }, |
| 253 | * ], |
| 254 | * permissionSets: [ |
| 255 | * { |
| 256 | * key: "admin", |
| 257 | * name: "AdministratorAccess", |
| 258 | * sessionDuration: "8 hours", |
| 259 | * }, |
| 260 | * ], |
| 261 | * assignments: [ |
| 262 | * { |
| 263 | * permissionSetKey: "admin", |
| 264 | * groupKey: "platform", |
| 265 | * accountKey: "prod", |
| 266 | * }, |
| 267 | * ], |
| 268 | * }, |
| 269 | * }); |
| 270 | * ``` |
| 271 | * |
| 272 | * @resource |
| 273 | */ |
| 274 | export const TenantRoot = Effect.fn(function* ( |
| 275 | id: string, |
| 276 | props: TenantRootProps = {}, |
| 277 | ) { |
| 278 | const sharedTags = props.tags ?? {}; |
| 279 | const organization = yield* AwsOrganization(`${id}Organization`, { |
| 280 | featureSet: "ALL", |
| 281 | ...props.organization, |
| 282 | }); |
| 283 | const root = yield* OrganizationRoot(`${id}Root`, { |
| 284 | ...props.root, |
| 285 | tags: mergeTags(sharedTags, props.root?.tags), |
| 286 | }); |
| 287 | |
| 288 | const policyTypes = yield* Effect.forEach( |
| 289 | props.policyTypes ?? ["SERVICE_CONTROL_POLICY"], |
| 290 | (policyType) => |
| 291 | OrganizationsRootPolicyType( |
| 292 | `${id}${toLogicalIdSegment(policyType)}PolicyType`, |
| 293 | { |
| 294 | rootId: root.rootId, |
| 295 | policyType, |
| 296 | }, |
| 297 | ), |
| 298 | { concurrency: "unbounded" }, |
| 299 | ); |
| 300 | |
| 301 | const trustedServicePrincipals = [ |
| 302 | ...(props.trustedServicePrincipals ?? []), |
| 303 | ...(props.identityCenter ? ["sso.amazonaws.com"] : []), |
| 304 | ]; |
| 305 | const trustedServiceAccess = yield* Effect.forEach( |
| 306 | [...new Set(trustedServicePrincipals)], |
| 307 | (servicePrincipal) => |
| 308 | OrganizationsTrustedServiceAccess( |
| 309 | `${id}${toLogicalIdSegment(servicePrincipal)}TrustedAccess`, |
| 310 | { servicePrincipal }, |
| 311 | ), |
| 312 | { concurrency: "unbounded" }, |
| 313 | ); |
| 314 | |
| 315 | const organizationalUnits: Record<string, OrganizationalUnit> = {}; |
| 316 | const accounts: Record<string, Account> = {}; |
| 317 | const targets: Record<TenantTargetKey, { targetId: any }> = { |
| 318 | root: { targetId: root.rootId as any }, |
| 319 | }; |
| 320 | |
| 321 | yield* createOrganizationalUnits({ |
| 322 | id, |
| 323 | parentId: root.rootId, |
| 324 | sharedTags, |
| 325 | specs: props.organizationalUnits ?? defaultTenantOrganizationalUnits(), |
| 326 | organizationalUnits, |
| 327 | accounts, |
| 328 | targets, |
| 329 | }); |
| 330 | |
| 331 | const delegatedAdministrators: DelegatedAdministrator[] = []; |
| 332 | const identityCenter = props.identityCenter |
| 333 | ? yield* createTenantIdentityCenter({ |
| 334 | id, |
| 335 | spec: props.identityCenter, |
| 336 | accounts, |
| 337 | sharedTags, |
| 338 | delegatedAdministrators, |
| 339 | }) |
| 340 | : undefined; |
| 341 | |
| 342 | const policies: Record<string, Policy> = {}; |
| 343 | const policyAttachments: PolicyAttachment[] = []; |
| 344 | for (const policySpec of props.policies ?? []) { |
| 345 | const policy = yield* OrganizationsPolicy( |
| 346 | `${id}${toLogicalIdSegment(policySpec.key)}Policy`, |
| 347 | { |
| 348 | name: policySpec.name, |
| 349 | description: policySpec.description, |
| 350 | type: policySpec.type ?? "SERVICE_CONTROL_POLICY", |
| 351 | document: policySpec.document, |
| 352 | tags: mergeTags(sharedTags, policySpec.tags), |
| 353 | }, |
| 354 | ); |
| 355 | policies[policySpec.key] = policy; |
| 356 | |
| 357 | for (const targetKey of policySpec.targetKeys) { |
| 358 | const target = targets[targetKey]; |
| 359 | if (!target) { |
| 360 | return yield* Effect.fail( |
| 361 | new Error( |
| 362 | `Unknown tenant policy target '${targetKey}' for policy '${policySpec.key}'`, |
| 363 | ), |
| 364 | ); |
| 365 | } |
| 366 | |
| 367 | policyAttachments.push( |
| 368 | yield* OrganizationsPolicyAttachment( |
| 369 | `${id}${toLogicalIdSegment(policySpec.key)}${toLogicalIdSegment(targetKey)}Attachment`, |
| 370 | { |
| 371 | policyId: policy.policyId, |
| 372 | targetId: target.targetId, |
| 373 | }, |
| 374 | ), |
| 375 | ); |
| 376 | } |
| 377 | } |
| 378 | |
| 379 | return { |
| 380 | organization, |
| 381 | root, |
| 382 | policyTypes, |
| 383 | trustedServiceAccess, |
| 384 | delegatedAdministrators, |
| 385 | organizationalUnits, |
| 386 | accounts, |
| 387 | policies, |
| 388 | policyAttachments, |
| 389 | identityCenter, |
| 390 | } satisfies TenantRootResult; |
| 391 | }); |
| 392 | |
| 393 | const createOrganizationalUnits = ({ |
| 394 | id, |
| 395 | parentId, |
| 396 | sharedTags, |
| 397 | specs, |
| 398 | organizationalUnits, |
| 399 | accounts, |
| 400 | targets, |
| 401 | }: { |
| 402 | id: string; |
| 403 | parentId: any; |
| 404 | sharedTags: Record<string, string>; |
| 405 | specs: TenantOrganizationalUnitSpec[]; |
| 406 | organizationalUnits: Record<string, OrganizationalUnit>; |
| 407 | accounts: Record<string, Account>; |
| 408 | targets: Record<TenantTargetKey, { targetId: any }>; |
| 409 | }): Effect.Effect<void, unknown, unknown> => |
| 410 | Effect.gen(function* () { |
| 411 | for (const spec of specs) { |
| 412 | const ou = yield* AwsOrganizationalUnit( |
| 413 | `${id}${toLogicalIdSegment(spec.key)}Ou`, |
| 414 | { |
| 415 | parentId, |
| 416 | name: spec.name ?? spec.key, |
| 417 | tags: mergeTags(sharedTags, spec.tags), |
| 418 | }, |
| 419 | ); |
| 420 | organizationalUnits[spec.key] = ou; |
| 421 | targets[spec.key] = { targetId: ou.ouId as any }; |
| 422 | |
| 423 | for (const accountSpec of spec.accounts ?? []) { |
| 424 | const account = yield* OrganizationAccount( |
| 425 | `${id}${toLogicalIdSegment(accountSpec.key)}Account`, |
| 426 | { |
| 427 | ...accountSpec, |
| 428 | parentId: ou.ouId, |
| 429 | tags: mergeTags(sharedTags, accountSpec.tags), |
| 430 | }, |
| 431 | ); |
| 432 | accounts[accountSpec.key] = account; |
| 433 | targets[accountSpec.key] = { targetId: account.accountId as any }; |
| 434 | } |
| 435 | |
| 436 | if (spec.children?.length) { |
| 437 | yield* createOrganizationalUnits({ |
| 438 | id, |
| 439 | parentId: ou.ouId as any, |
| 440 | sharedTags, |
| 441 | specs: spec.children, |
| 442 | organizationalUnits, |
| 443 | accounts, |
| 444 | targets, |
| 445 | }); |
| 446 | } |
| 447 | } |
| 448 | }); |
| 449 | |
| 450 | const createTenantIdentityCenter = Effect.fn(function* ({ |
| 451 | id, |
| 452 | spec, |
| 453 | accounts, |
| 454 | delegatedAdministrators, |
| 455 | }: { |
| 456 | id: string; |
| 457 | spec: TenantIdentityCenterSpec; |
| 458 | accounts: Record<string, Account>; |
| 459 | sharedTags: Record<string, string>; |
| 460 | delegatedAdministrators: DelegatedAdministrator[]; |
| 461 | }) { |
| 462 | const instance = yield* IdentityCenter.Instance(`${id}IdentityCenter`, { |
| 463 | mode: spec.mode ?? "existing", |
| 464 | instanceArn: spec.instanceArn, |
| 465 | name: spec.name, |
| 466 | }); |
| 467 | |
| 468 | if (spec.delegatedAdminAccountKey) { |
| 469 | const account = accounts[spec.delegatedAdminAccountKey]; |
| 470 | if (!account) { |
| 471 | return yield* Effect.fail( |
| 472 | new Error( |
| 473 | `Unknown delegated admin account '${spec.delegatedAdminAccountKey}'`, |
| 474 | ), |
| 475 | ); |
| 476 | } |
| 477 | delegatedAdministrators.push( |
| 478 | yield* OrganizationsDelegatedAdministrator( |
| 479 | `${id}${toLogicalIdSegment(spec.delegatedAdminAccountKey)}IdentityCenterDelegatedAdmin`, |
| 480 | { |
| 481 | accountId: account.accountId, |
| 482 | servicePrincipal: "sso.amazonaws.com", |
| 483 | }, |
| 484 | ), |
| 485 | ); |
| 486 | } |
| 487 | |
| 488 | const groups: Record<string, IdentityCenter.Group> = {}; |
| 489 | for (const groupSpec of spec.groups ?? []) { |
| 490 | groups[groupSpec.key] = yield* IdentityCenter.Group( |
| 491 | `${id}${toLogicalIdSegment(groupSpec.key)}Group`, |
| 492 | { |
| 493 | identityStoreId: instance.identityStoreId, |
| 494 | displayName: groupSpec.displayName, |
| 495 | description: groupSpec.description, |
| 496 | }, |
| 497 | ); |
| 498 | } |
| 499 | |
| 500 | const permissionSets: Record<string, IdentityCenter.PermissionSet> = {}; |
| 501 | for (const permissionSetSpec of spec.permissionSets ?? []) { |
| 502 | permissionSets[permissionSetSpec.key] = yield* IdentityCenter.PermissionSet( |
| 503 | `${id}${toLogicalIdSegment(permissionSetSpec.key)}PermissionSet`, |
| 504 | { |
| 505 | instanceArn: instance.instanceArn, |
| 506 | name: permissionSetSpec.name, |
| 507 | description: permissionSetSpec.description, |
| 508 | sessionDuration: permissionSetSpec.sessionDuration, |
| 509 | relayState: permissionSetSpec.relayState, |
| 510 | }, |
| 511 | ); |
| 512 | } |
| 513 | |
| 514 | const assignments: Record<string, IdentityCenter.AccountAssignment> = {}; |
| 515 | for (const assignmentSpec of spec.assignments ?? []) { |
| 516 | const account = accounts[assignmentSpec.accountKey]; |
| 517 | if (!account) { |
| 518 | return yield* Effect.fail( |
| 519 | new Error(`Unknown assignment account '${assignmentSpec.accountKey}'`), |
| 520 | ); |
| 521 | } |
| 522 | const permissionSet = permissionSets[assignmentSpec.permissionSetKey]; |
| 523 | if (!permissionSet) { |
| 524 | return yield* Effect.fail( |
| 525 | new Error( |
| 526 | `Unknown assignment permission set '${assignmentSpec.permissionSetKey}'`, |
| 527 | ), |
| 528 | ); |
| 529 | } |
| 530 | const principalId = |
| 531 | assignmentSpec.groupKey !== undefined |
| 532 | ? groups[assignmentSpec.groupKey]?.groupId |
| 533 | : assignmentSpec.principalId; |
| 534 | if (!principalId) { |
| 535 | return yield* Effect.fail( |
| 536 | new Error( |
| 537 | `Unable to resolve principal for assignment '${assignmentSpec.key ?? `${assignmentSpec.permissionSetKey}-${assignmentSpec.accountKey}`}'`, |
| 538 | ), |
| 539 | ); |
| 540 | } |
| 541 | |
| 542 | const key = |
| 543 | assignmentSpec.key ?? |
| 544 | `${assignmentSpec.permissionSetKey}-${assignmentSpec.accountKey}-${assignmentSpec.groupKey ?? principalId}`; |
| 545 | |
| 546 | assignments[key] = yield* IdentityCenter.AccountAssignment( |
| 547 | `${id}${toLogicalIdSegment(key)}Assignment`, |
| 548 | { |
| 549 | instanceArn: instance.instanceArn, |
| 550 | permissionSetArn: permissionSet.permissionSetArn, |
| 551 | principalId, |
| 552 | principalType: assignmentSpec.groupKey |
| 553 | ? "GROUP" |
| 554 | : (assignmentSpec.principalType ?? "USER"), |
| 555 | targetId: account.accountId, |
| 556 | }, |
| 557 | ); |
| 558 | } |
| 559 | |
| 560 | return { |
| 561 | instance, |
| 562 | groups, |
| 563 | permissionSets, |
| 564 | assignments, |
| 565 | }; |
| 566 | }); |
| 567 | |
| 568 | const mergeTags = ( |
| 569 | shared: Record<string, string>, |
| 570 | tags: Record<string, string> | undefined, |
| 571 | ) => ({ |
| 572 | ...shared, |
| 573 | ...tags, |
| 574 | }); |