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 } from "./common.ts";
8
9export interface TrustedServiceAccessProps {
10 /**
11 * Service principal granted trusted access to the organization.
12 */
13 servicePrincipal: string;
14}
15
16export interface TrustedServiceAccess extends Resource<
17 "AWS.Organizations.TrustedServiceAccess",
18 TrustedServiceAccessProps,
19 {
20 /**
21 * Service principal granted trusted access.
22 */
23 servicePrincipal: string;
24 /**
25 * When trusted access was enabled.
26 */
27 dateEnabled: Date | undefined;
28 },
29 never,
30 Providers
31> {}
32
33/**
34 * Enables trusted access for an AWS service principal, allowing that service
35 * to operate across all accounts in the organization.
36 *
37 * Typically paired with a {@link DelegatedAdministrator} that hands day-to-day
38 * administration of the service to a member account. Existence-only resource:
39 * changing `servicePrincipal` replaces it.
40 * ### Enabling Trusted Access
41 * **Example:** Enable IAM Identity Center
42 * ```typescript
43 * yield* TrustedServiceAccess("SsoTrustedAccess", {
44 * servicePrincipal: "sso.amazonaws.com",
45 * });
46 * ```
47 *
48 * **Example:** Trusted Access Plus a Delegated Administrator
49 * ```typescript
50 * const guardDutyAccess = yield* TrustedServiceAccess("GuardDutyAccess", {
51 * servicePrincipal: "guardduty.amazonaws.com",
52 * });
53 *
54 * yield* DelegatedAdministrator("GuardDutyAdmin", {
55 * accountId: securityAccount.accountId,
56 * servicePrincipal: guardDutyAccess.servicePrincipal,
57 * });
58 * ```
59 *
60 * @resource
61 */
62export const TrustedServiceAccess = Resource<TrustedServiceAccess>(
63 "AWS.Organizations.TrustedServiceAccess",
64);
65
66export const TrustedServiceAccessProvider = () =>
67 Provider.effect(
68 TrustedServiceAccess,
69 Effect.gen(function* () {
70 return {
71 stables: ["servicePrincipal"],
72 diff: Effect.fn(function* ({ olds, news }) {
73 if (!isResolved(news)) return;
74 if (olds?.servicePrincipal !== news.servicePrincipal) {
75 return { action: "replace" } as const;
76 }
77 }),
78 read: Effect.fn(function* ({ olds, output }) {
79 const servicePrincipal =
80 output?.servicePrincipal ?? olds?.servicePrincipal;
81 if (servicePrincipal === undefined) {
82 // Output-valued props don't survive a `creating`-state round-trip
83 // (they deserialize as `undefined`) — report "not found" so the
84 // engine re-drives the create.
85 return undefined;
86 }
87 return yield* readTrustedServiceAccess(servicePrincipal);
88 }),
89 list: () =>
90 Effect.gen(function* () {
91 // Enumerate every service principal granted trusted access to the
92 // organization. The list response already carries the full `read`
93 // shape (servicePrincipal + dateEnabled), so each enabled principal
94 // maps directly to one Attributes — no per-item hydration needed.
95 const principals = yield* retryOrganizations(
96 collectPages(
97 (NextToken) =>
98 organizations.listAWSServiceAccessForOrganization({
99 NextToken,
100 }),
101 (page) => page.EnabledServicePrincipals,
102 ),
103 );
104
105 return principals
106 .filter(
107 (
108 candidate,
109 ): candidate is organizations.EnabledServicePrincipal & {
110 ServicePrincipal: string;
111 } => candidate.ServicePrincipal != null,
112 )
113 .map(
114 (candidate) =>
115 ({
116 servicePrincipal: candidate.ServicePrincipal,
117 dateEnabled: candidate.DateEnabled,
118 }) satisfies TrustedServiceAccess["Attributes"],
119 );
120 }).pipe(
121 // Not an org management account (or lacking access) — there's no
122 // organization to enumerate, so degrade to an empty list.
123 Effect.catchTags({
124 AWSOrganizationsNotInUseException: () =>
125 Effect.succeed([] as TrustedServiceAccess["Attributes"][]),
126 AccessDeniedException: () =>
127 Effect.succeed([] as TrustedServiceAccess["Attributes"][]),
128 }),
129 ),
130 reconcile: Effect.fn(function* ({ news, session }) {
131 // Observe — fetch live trusted-access state. We never trust prior
132 // `output` blindly; if access was disabled out-of-band we re-enable.
133 let state = yield* readTrustedServiceAccess(news.servicePrincipal);
134
135 // Ensure — enable trusted access if it's missing. This API is
136 // effectively idempotent for already-enabled principals, but we
137 // gate the call to avoid unnecessary churn.
138 if (!state) {
139 yield* retryOrganizations(
140 organizations.enableAWSServiceAccess({
141 ServicePrincipal: news.servicePrincipal,
142 }),
143 );
144 state = yield* readTrustedServiceAccess(news.servicePrincipal);
145 if (!state) {
146 return yield* Effect.fail(
147 new Error(
148 `trusted service access '${news.servicePrincipal}' not found after create`,
149 ),
150 );
151 }
152 }
153
154 yield* session.note(state.servicePrincipal);
155 return state;
156 }),
157 delete: Effect.fn(function* ({ output }) {
158 if (!(yield* readTrustedServiceAccess(output.servicePrincipal))) {
159 return;
160 }
161
162 yield* retryOrganizations(
163 organizations.disableAWSServiceAccess({
164 ServicePrincipal: output.servicePrincipal,
165 }),
166 );
167 }),
168 };
169 }),
170 );
171
172const readTrustedServiceAccess = Effect.fn(function* (
173 servicePrincipal: string,
174) {
175 const principals = yield* retryOrganizations(
176 collectPages(
177 (NextToken) =>
178 organizations.listAWSServiceAccessForOrganization({ NextToken }),
179 (page) => page.EnabledServicePrincipals,
180 ),
181 );
182
183 const match = principals.find(
184 (candidate) => candidate.ServicePrincipal === servicePrincipal,
185 );
186
187 return match
188 ? ({
189 servicePrincipal,
190 dateEnabled: match.DateEnabled,
191 } satisfies TrustedServiceAccess["Attributes"])
192 : undefined;
193});