1import * as organizations from "@distilled.cloud/aws/organizations";
2import type * as Duration from "effect/Duration";
3import * as Effect from "effect/Effect";
4import type { ServiceControlPolicyDocument } from "../IAM/Policy.ts";
5import * as IdentityCenter from "../IdentityCenter/index.ts";
6import type { Account, AccountProps } from "./Account.ts";
7import { Account as OrganizationAccount } from "./Account.ts";
8import type { DelegatedAdministrator } from "./DelegatedAdministrator.ts";
9import { DelegatedAdministrator as OrganizationsDelegatedAdministrator } from "./DelegatedAdministrator.ts";
10import type { Organization, OrganizationProps } from "./Organization.ts";
11import { Organization as AwsOrganization } from "./Organization.ts";
12import type {
13 OrganizationalUnit,
14 OrganizationalUnitProps,
15} from "./OrganizationalUnit.ts";
16import { OrganizationalUnit as AwsOrganizationalUnit } from "./OrganizationalUnit.ts";
17import type { Policy } from "./Policy.ts";
18import { Policy as OrganizationsPolicy } from "./Policy.ts";
19import type { PolicyAttachment } from "./PolicyAttachment.ts";
20import { PolicyAttachment as OrganizationsPolicyAttachment } from "./PolicyAttachment.ts";
21import type { Root, RootProps } from "./Root.ts";
22import { Root as OrganizationRoot } from "./Root.ts";
23import type { RootPolicyType } from "./RootPolicyType.ts";
24import { RootPolicyType as OrganizationsRootPolicyType } from "./RootPolicyType.ts";
25import type { TrustedServiceAccess } from "./TrustedServiceAccess.ts";
26import { TrustedServiceAccess as OrganizationsTrustedServiceAccess } from "./TrustedServiceAccess.ts";
27
28export type TenantTargetKey = "root" | string;
29
30export 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
42export 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
59export 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
83export 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
92export 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
109export 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
127export 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
148export 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
170export 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
188const 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
229const 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 */
274export 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
393const 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
450const 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
568const mergeTags = (
569 shared: Record<string, string>,
570 tags: Record<string, string> | undefined,
571) => ({
572 ...shared,
573 ...tags,
574});