1import * as lf from "@distilled.cloud/aws/lakeformation";
2import * as Effect from "effect/Effect";
3import * as Stream from "effect/Stream";
4import { isResolved } from "../../Diff.ts";
5import * as Provider from "../../Provider.ts";
6import { Resource } from "../../Resource.ts";
7import type { Providers } from "../Providers.ts";
8import { retryWhileInvalidPrincipal } from "./internal.ts";
9import {
10 type LakeFormationResourceSpec,
11 toWireResource,
12} from "./ResourceSpec.ts";
13
14export interface OptInProps {
15 /**
16 * The principal (IAM user/role ARN or `IAM_ALLOWED_PRINCIPALS`) whose
17 * access to the resource is opted into Lake Formation enforcement.
18 * Changing it replaces the opt-in.
19 */
20 principal: string;
21 /**
22 * The Data Catalog resource (database, table, …) to enforce Lake
23 * Formation permissions on for the principal. Changing it replaces the
24 * opt-in.
25 */
26 resource: LakeFormationResourceSpec;
27}
28
29export interface OptIn extends Resource<
30 "AWS.LakeFormation.OptIn",
31 OptInProps,
32 {
33 principal: string;
34 resource: lf.Resource;
35 },
36 {},
37 Providers
38> {}
39
40/**
41 * A Lake Formation opt-in — enforces Lake Formation permissions for one
42 * principal on one Data Catalog resource while the account is in hybrid
43 * access mode (where IAM/S3 policies otherwise govern access).
44 *
45 * ### Opting Into Lake Formation Enforcement
46 * **Example:** Enforce Lake Formation for a Role on a Database
47 * ```typescript
48 * import * as AWS from "alchemy/AWS";
49 *
50 * const optIn = yield* AWS.LakeFormation.OptIn("AnalystOptIn", {
51 * principal: analystRole.roleArn,
52 * resource: { database: { name: database.databaseName } },
53 * });
54 * ```
55 *
56 * @resource
57 */
58export const OptIn = Resource<OptIn>("AWS.LakeFormation.OptIn");
59
60export const OptInProvider = () =>
61 Provider.effect(
62 OptIn,
63 Effect.gen(function* () {
64 const observe = Effect.fn(function* (
65 principal: string,
66 resource: lf.Resource,
67 ) {
68 const pages = yield* lf.listLakeFormationOptIns
69 .pages({
70 Principal: { DataLakePrincipalIdentifier: principal },
71 Resource: resource,
72 })
73 .pipe(
74 Stream.runCollect,
75 // a deleted resource (e.g. the Glue database is already gone)
76 // means the opt-in no longer exists either; a deleted/not-yet-
77 // propagated IAM principal likewise has no visible opt-ins
78 Effect.catchTag(
79 ["EntityNotFoundException", "InvalidLakeFormationPrincipal"],
80 () => Effect.succeed([]),
81 ),
82 );
83 return Array.from(pages)
84 .flatMap((page) => page.LakeFormationOptInsInfoList ?? [])
85 .find(
86 (info) => info.Principal?.DataLakePrincipalIdentifier === principal,
87 );
88 });
89
90 return OptIn.Provider.of({
91 stables: ["principal", "resource"],
92
93 list: () =>
94 Effect.gen(function* () {
95 const pages = yield* lf.listLakeFormationOptIns
96 .pages({})
97 .pipe(Stream.runCollect);
98 return Array.from(pages)
99 .flatMap((page) => page.LakeFormationOptInsInfoList ?? [])
100 .filter(
101 (info) =>
102 info.Principal?.DataLakePrincipalIdentifier !== undefined &&
103 info.Resource !== undefined,
104 )
105 .map((info) => ({
106 principal: info.Principal!.DataLakePrincipalIdentifier!,
107 resource: info.Resource!,
108 }));
109 }),
110
111 read: Effect.fn(function* ({ olds, output }) {
112 const principal = output?.principal ?? olds?.principal;
113 const resource =
114 output?.resource ??
115 (olds !== undefined ? toWireResource(olds.resource) : undefined);
116 if (principal === undefined || resource === undefined) {
117 return undefined;
118 }
119 const found = yield* observe(principal, resource);
120 if (found === undefined) return undefined;
121 // Opt-ins are not taggable — ownership cannot be verified.
122 return { principal, resource };
123 }),
124
125 diff: Effect.fn(function* ({ news, olds }) {
126 if (!isResolved(news)) return undefined;
127 if (
128 news.principal !== olds.principal ||
129 JSON.stringify(toWireResource(news.resource)) !==
130 JSON.stringify(toWireResource(olds.resource))
131 ) {
132 // an opt-in has no mutable body — any change replaces it
133 return { action: "replace" } as const;
134 }
135 }),
136
137 reconcile: Effect.fn(function* ({ news, session }) {
138 const resource = toWireResource(news.resource);
139
140 // 1. OBSERVE
141 const found = yield* observe(news.principal, resource);
142
143 // 2. ENSURE — existence-only resource, nothing to sync. A freshly
144 // created IAM principal takes ~10s to propagate before Lake
145 // Formation accepts it, so retry through the typed rejection.
146 if (found === undefined) {
147 yield* lf
148 .createLakeFormationOptIn({
149 Principal: { DataLakePrincipalIdentifier: news.principal },
150 Resource: resource,
151 })
152 .pipe(retryWhileInvalidPrincipal);
153 }
154
155 yield* session.note(news.principal);
156 return { principal: news.principal, resource };
157 }),
158
159 delete: Effect.fn(function* ({ output }) {
160 yield* lf
161 .deleteLakeFormationOptIn({
162 Principal: {
163 DataLakePrincipalIdentifier: output.principal,
164 },
165 Resource: output.resource,
166 })
167 .pipe(
168 // the opt-in (or its principal) is already gone
169 Effect.catchTag(
170 ["EntityNotFoundException", "InvalidLakeFormationPrincipal"],
171 () => Effect.void,
172 ),
173 );
174 }),
175 });
176 }),
177 );