1import * as fms from "@distilled.cloud/aws/fms";
2import * as Effect from "effect/Effect";
3import * as Schedule from "effect/Schedule";
4import { Unowned } from "../../AdoptPolicy.ts";
5import * as Provider from "../../Provider.ts";
6import { Resource } from "../../Resource.ts";
7import { AWSEnvironment } from "../Environment.ts";
8import type { Providers } from "../Providers.ts";
9import { Region } from "../Region.ts";
10
11// FMS admin-account management (get/associate/disassociate AdminAccount) is a
12// global, organization-level operation served exclusively from the us-east-1
13// endpoint — every other region rejects it with `InvalidOperationException:
14// This operation is not supported in the '<region>' region.` Pin every
15// control-plane call there regardless of the ambient stack region (same
16// pattern as Cost Explorer / ECR Public / WAFv2 CloudFront-scope).
17const FMS_ADMIN_REGION = "us-east-1";
18
19/** Pin an FMS admin-account operation to the us-east-1 endpoint. */
20export const pinFms = <A, E, R>(effect: Effect.Effect<A, E, R>) =>
21 effect.pipe(Effect.provideService(Region, Effect.succeed(FMS_ADMIN_REGION)));
22
23export interface AdminAccountProps {
24 /**
25 * The AWS account ID to designate as the AWS Firewall Manager administrator
26 * account for the organization. Must be an account in the same AWS
27 * Organization as the caller (which must be the Organizations management
28 * account). If omitted, the caller's account is used.
29 */
30 adminAccount?: string;
31}
32
33/** @resource */
34export interface AdminAccount extends Resource<
35 "AWS.FMS.AdminAccount",
36 AdminAccountProps,
37 {
38 /** The account designated as the FMS administrator. */
39 adminAccount: string;
40 /** Status of the FMS administrator IAM role (`READY` / `CREATING` / ...). */
41 roleStatus: string | undefined;
42 },
43 never,
44 Providers
45> {}
46
47/**
48 * The AWS Firewall Manager administrator account — an organization-level
49 * singleton that designates which account manages FMS security policies.
50 *
51 * :::caution
52 * FMS requires the caller to be the AWS Organizations **management account**,
53 * and the organization must have AWS Config enabled. On accounts that are not
54 * an Organizations management account this resource fails at association time
55 * with a typed `InvalidOperationException`.
56 *
57 * FMS admin-account APIs are served only from the us-east-1 endpoint; this
58 * resource pins every control-plane call there regardless of the ambient
59 * stack region.
60 * :::
61 *
62 * This is a capture-and-restore singleton: FMS exposes no tags, so ownership is
63 * tracked by Alchemy state — adopting a pre-existing admin account that Alchemy
64 * did not create requires `--adopt`, and destroy disassociates the admin.
65 *
66 * ### Designating the FMS admin
67 * **Example:** Designate the caller as the FMS admin
68 * ```typescript
69 * const admin = yield* FMS.AdminAccount("FmsAdmin", {});
70 * ```
71 *
72 * **Example:** Designate a specific member account
73 * ```typescript
74 * const admin = yield* FMS.AdminAccount("FmsAdmin", {
75 * adminAccount: "123456789012",
76 * });
77 * ```
78 */
79const AdminAccountResource = Resource<AdminAccount>("AWS.FMS.AdminAccount");
80
81export { AdminAccountResource as AdminAccount };
82
83// `getAdminAccount` throws `ResourceNotFoundException` when no FMS admin has
84// been designated — collapse to `undefined`.
85const getAdmin = pinFms(fms.getAdminAccount({})).pipe(
86 Effect.map((r) => r as fms.GetAdminAccountResponse | undefined),
87 Effect.catchTag("ResourceNotFoundException", () => Effect.succeed(undefined)),
88);
89
90export const AdminAccountProvider = () =>
91 Provider.effect(
92 AdminAccountResource,
93 Effect.gen(function* () {
94 // Association is asynchronous; the FMS role transitions CREATING → READY.
95 const waitUntilReady = getAdmin.pipe(
96 Effect.repeat({
97 schedule: Schedule.spaced("3 seconds"),
98 until: (a) => a?.RoleStatus === "READY",
99 times: 20,
100 }),
101 );
102
103 return {
104 read: Effect.fn(function* ({ output }) {
105 const admin = yield* getAdmin;
106 if (!admin?.AdminAccount) return undefined;
107 const attrs = {
108 adminAccount: admin.AdminAccount,
109 roleStatus: admin.RoleStatus,
110 };
111 // FMS admin has no tags — ownership can't be verified from the cloud.
112 // With no prior state, treat an existing admin as foreign.
113 return output ? attrs : Unowned(attrs);
114 }),
115
116 // Organization-level singleton — report the single admin account.
117 list: () =>
118 getAdmin.pipe(
119 Effect.map((a) =>
120 a?.AdminAccount
121 ? [{ adminAccount: a.AdminAccount, roleStatus: a.RoleStatus }]
122 : [],
123 ),
124 ),
125
126 reconcile: Effect.fn(function* ({ news = {}, session }) {
127 const { accountId } = yield* AWSEnvironment.current;
128 const desired = news.adminAccount ?? accountId;
129
130 // 1. OBSERVE
131 let admin = yield* getAdmin;
132
133 // 2. ENSURE — associate the admin account if none is set.
134 if (!admin?.AdminAccount) {
135 // On a fresh account the first associate call kicks off creation
136 // of the AWSServiceRoleForFMS service-linked role and fails
137 // transiently while it materializes — either with a bare
138 // `InvalidOperationException: Operation is invalid.` or with
139 // `Service role name AWSServiceRoleForFMS already exists ...
140 // Otherwise, try your request again.` Both are eventual-
141 // consistency races, not terminal failures — retry bounded.
142 yield* pinFms(
143 fms.associateAdminAccount({ AdminAccount: desired }),
144 ).pipe(
145 Effect.retry({
146 // `: boolean` blocks TS 5.5+ inferred type predicates — see
147 // the note on the disassociate retry in `delete` below.
148 while: (e): boolean =>
149 e._tag === "InvalidOperationException" &&
150 (e.message === "Operation is invalid." ||
151 (e.message?.includes("AWSServiceRoleForFMS") ?? false)),
152 schedule: Schedule.spaced("5 seconds"),
153 times: 8,
154 }),
155 );
156 admin = yield* waitUntilReady;
157 }
158
159 // 3. RETURN fresh attributes.
160 const final = yield* getAdmin;
161 yield* session.note(desired);
162 return {
163 adminAccount: final?.AdminAccount ?? desired,
164 roleStatus: final?.RoleStatus,
165 };
166 }),
167
168 delete: Effect.fn(function* () {
169 // For several minutes after association FMS reports the org as
170 // "currently onboarding with AWS Firewall Manager and cannot be
171 // offboarded" (a typed `InvalidOperationException`). Retry bounded;
172 // onboarding can take >10 minutes, so a destroy issued immediately
173 // after create may need to be re-run once onboarding settles.
174 yield* pinFms(fms.disassociateAdminAccount({})).pipe(
175 Effect.retry({
176 // `: boolean` prevents TS 5.5+ from inferring a type predicate
177 // for this lambda — an inferred `e is InvalidOperationException`
178 // refinement makes `Effect.retry` collapse the error channel to
179 // `unknown`, which breaks the `catchTag` below.
180 while: (e): boolean =>
181 e._tag === "InvalidOperationException" &&
182 (e.message?.includes("cannot be offboarded") ?? false),
183 schedule: Schedule.spaced("8 seconds"),
184 times: 8,
185 }),
186 Effect.catchTag("ResourceNotFoundException", () => Effect.void),
187 );
188 }),
189 };
190 }),
191 );