1import * as organizations from "@distilled.cloud/aws/organizations";
2import * as Effect from "effect/Effect";
3import { isResolved } from "../../Diff.ts";
4import * as Provider from "../../Provider.ts";
5import { Resource } from "../../Resource.ts";
6import type { Providers } from "../Providers.ts";
7import { collectPages, retryOrganizations, unredact } from "./common.ts";
8
9export interface DelegatedAdministratorProps {
10 /**
11 * Member account registered as delegated administrator.
12 */
13 accountId: string;
14 /**
15 * Service principal delegated to the account.
16 */
17 servicePrincipal: string;
18}
19
20export interface DelegatedAdministrator extends Resource<
21 "AWS.Organizations.DelegatedAdministrator",
22 DelegatedAdministratorProps,
23 {
24 /**
25 * 12-digit ID of the delegated administrator account.
26 */
27 accountId: string;
28 /**
29 * ARN of the delegated administrator account.
30 */
31 accountArn: string | undefined;
32 /**
33 * Friendly name of the delegated administrator account.
34 */
35 accountName: string | undefined;
36 /**
37 * Email address of the delegated administrator account.
38 */
39 accountEmail: string | undefined;
40 /**
41 * Service principal delegated to the account.
42 */
43 servicePrincipal: string;
44 /**
45 * When the delegation was enabled.
46 */
47 delegationEnabledDate: Date | undefined;
48 },
49 never,
50 Providers
51> {}
52
53/**
54 * Registers a member {@link Account} as the delegated administrator for a
55 * trusted AWS service, letting that account manage the service org-wide
56 * instead of the management account.
57 *
58 * Requires trusted access for the same service principal (see
59 * {@link TrustedServiceAccess}). Existence-only resource: changing
60 * `accountId` or `servicePrincipal` replaces the registration.
61 * ### Delegating Administration
62 * **Example:** Delegate GuardDuty to a Security Account
63 * ```typescript
64 * const security = yield* Account("Security", {
65 * name: "security",
66 * email: "aws-security@example.com",
67 * parentId: root.rootId,
68 * });
69 *
70 * const guardDutyAccess = yield* TrustedServiceAccess("GuardDutyAccess", {
71 * servicePrincipal: "guardduty.amazonaws.com",
72 * });
73 *
74 * yield* DelegatedAdministrator("GuardDutyAdmin", {
75 * accountId: security.accountId,
76 * servicePrincipal: guardDutyAccess.servicePrincipal,
77 * });
78 * ```
79 *
80 * @resource
81 */
82export const DelegatedAdministrator = Resource<DelegatedAdministrator>(
83 "AWS.Organizations.DelegatedAdministrator",
84);
85
86export const DelegatedAdministratorProvider = () =>
87 Provider.effect(
88 DelegatedAdministrator,
89 Effect.gen(function* () {
90 return {
91 stables: ["accountId", "servicePrincipal"],
92 diff: Effect.fn(function* ({ olds, news }) {
93 if (!isResolved(news)) return;
94 if (
95 olds?.accountId !== news.accountId ||
96 olds?.servicePrincipal !== news.servicePrincipal
97 ) {
98 return { action: "replace" } as const;
99 }
100 }),
101 read: Effect.fn(function* ({ olds, output }) {
102 const accountId = output?.accountId ?? olds?.accountId;
103 const servicePrincipal =
104 output?.servicePrincipal ?? olds?.servicePrincipal;
105 if (accountId === undefined || servicePrincipal === undefined) {
106 // Output-valued props don't survive a `creating`-state round-trip
107 // (they deserialize as `undefined`) — report "not found" so the
108 // engine re-drives the create.
109 return undefined;
110 }
111 return yield* readDelegatedAdministrator({
112 accountId,
113 servicePrincipal,
114 });
115 }),
116 list: () =>
117 Effect.gen(function* () {
118 // Enumerate every delegated administrator account in the org. If
119 // this account isn't an org management account (or lacks access),
120 // Organizations enumeration isn't available here — return [].
121 const admins = yield* retryOrganizations(
122 collectPages(
123 (NextToken) =>
124 organizations.listDelegatedAdministrators({ NextToken }),
125 (page) => page.DelegatedAdministrators,
126 ),
127 ).pipe(
128 Effect.catchTags({
129 AWSOrganizationsNotInUseException: () =>
130 Effect.succeed<organizations.DelegatedAdministrator[]>([]),
131 AccessDeniedException: () =>
132 Effect.succeed<organizations.DelegatedAdministrator[]>([]),
133 UnsupportedAPIEndpointException: () =>
134 Effect.succeed<organizations.DelegatedAdministrator[]>([]),
135 }),
136 );
137
138 // Fan out: each admin account may be delegated for multiple
139 // services. The canonical `read` shape is one item per
140 // (account, servicePrincipal), so expand each account into one
141 // Attributes per delegated service.
142 const rows = yield* Effect.forEach(
143 admins.filter(
144 (
145 admin,
146 ): admin is organizations.DelegatedAdministrator & {
147 Id: string;
148 } => admin.Id != null,
149 ),
150 (admin) =>
151 retryOrganizations(
152 collectPages(
153 (NextToken) =>
154 organizations.listDelegatedServicesForAccount({
155 AccountId: admin.Id,
156 NextToken,
157 }),
158 (page) => page.DelegatedServices,
159 ),
160 ).pipe(
161 // The account may be deregistered between the two calls — skip.
162 Effect.catchTags({
163 AccountNotFoundException: () =>
164 Effect.succeed<organizations.DelegatedService[]>([]),
165 AccountNotRegisteredException: () =>
166 Effect.succeed<organizations.DelegatedService[]>([]),
167 }),
168 Effect.map((services) =>
169 services
170 .filter(
171 (
172 service,
173 ): service is organizations.DelegatedService & {
174 ServicePrincipal: string;
175 } => service.ServicePrincipal != null,
176 )
177 .map(
178 (service) =>
179 ({
180 accountId: admin.Id,
181 accountArn: admin.Arn,
182 accountName: unredact(admin.Name),
183 accountEmail: unredact(admin.Email),
184 servicePrincipal: service.ServicePrincipal,
185 delegationEnabledDate:
186 service.DelegationEnabledDate ??
187 admin.DelegationEnabledDate,
188 }) satisfies DelegatedAdministrator["Attributes"],
189 ),
190 ),
191 ),
192 { concurrency: 10 },
193 );
194
195 return rows.flat();
196 }),
197 reconcile: Effect.fn(function* ({ news, session }) {
198 // Observe — look up the live delegation. Replacement-only props
199 // (`accountId`, `servicePrincipal`) mean the diff handles identity
200 // changes; we just check whether the registration exists.
201 let state = yield* readDelegatedAdministrator(news);
202
203 // Ensure — register if missing. Tolerate
204 // `AccountAlreadyRegisteredException` for idempotency under races
205 // between observe and register.
206 if (!state) {
207 yield* retryOrganizations(
208 organizations
209 .registerDelegatedAdministrator({
210 AccountId: news.accountId,
211 ServicePrincipal: news.servicePrincipal,
212 })
213 .pipe(
214 Effect.catchTag(
215 "AccountAlreadyRegisteredException",
216 () => Effect.void,
217 ),
218 ),
219 );
220 state = yield* readDelegatedAdministrator(news);
221 if (!state) {
222 return yield* Effect.fail(
223 new Error(
224 `delegated administrator '${news.accountId}' for '${news.servicePrincipal}' not found after create`,
225 ),
226 );
227 }
228 }
229
230 yield* session.note(`${state.accountId}:${state.servicePrincipal}`);
231 return state;
232 }),
233 delete: Effect.fn(function* ({ output }) {
234 if (
235 !(yield* readDelegatedAdministrator({
236 accountId: output.accountId,
237 servicePrincipal: output.servicePrincipal,
238 }))
239 ) {
240 return;
241 }
242
243 yield* retryOrganizations(
244 organizations.deregisterDelegatedAdministrator({
245 AccountId: output.accountId,
246 ServicePrincipal: output.servicePrincipal,
247 }),
248 );
249 }),
250 };
251 }),
252 );
253
254const readDelegatedAdministrator = Effect.fn(function* ({
255 accountId,
256 servicePrincipal,
257}: DelegatedAdministratorProps) {
258 const [delegatedServices, delegatedAdmins] = yield* Effect.all([
259 retryOrganizations(
260 collectPages(
261 (NextToken) =>
262 organizations.listDelegatedServicesForAccount({
263 AccountId: accountId,
264 NextToken,
265 }),
266 (page) => page.DelegatedServices,
267 ),
268 ),
269 retryOrganizations(
270 collectPages(
271 (NextToken) =>
272 organizations.listDelegatedAdministrators({
273 ServicePrincipal: servicePrincipal,
274 NextToken,
275 }),
276 (page) => page.DelegatedAdministrators,
277 ),
278 ),
279 ]);
280
281 const service = delegatedServices.find(
282 (candidate) => candidate.ServicePrincipal === servicePrincipal,
283 );
284 const account = delegatedAdmins.find(
285 (candidate) => candidate.Id === accountId,
286 );
287
288 return service && account
289 ? ({
290 accountId,
291 accountArn: account.Arn,
292 accountName: unredact(account.Name),
293 accountEmail: unredact(account.Email),
294 servicePrincipal,
295 delegationEnabledDate:
296 service.DelegationEnabledDate ?? account.DelegationEnabledDate,
297 } satisfies DelegatedAdministrator["Attributes"])
298 : undefined;
299});