1import * as lf from "@distilled.cloud/aws/lakeformation";
2import * as Effect from "effect/Effect";
3import * as Schedule from "effect/Schedule";
4import * as Stream from "effect/Stream";
5
6/**
7 * Bounded retry through `ConcurrentModificationException` — Lake Formation
8 * serializes grant/revoke/settings mutations and rejects concurrent writers.
9 * Explicitly typed so declaration emit does not widen the provider layer (see
10 * PATTERNS §7 on inlined `Effect.retry`).
11 */
12export const retryWhileConcurrentModification = <
13 A,
14 E extends { _tag: string },
15 R,
16>(
17 self: Effect.Effect<A, E, R>,
18): Effect.Effect<A, E, R> =>
19 Effect.retry(self, {
20 while: (e) => e._tag === "ConcurrentModificationException",
21 schedule: Schedule.max([Schedule.fixed("2 seconds"), Schedule.recurs(10)]),
22 });
23
24/**
25 * Bounded retry through `InvalidLakeFormationPrincipal` — a freshly-created
26 * IAM role/user takes ~10s to propagate before Lake Formation accepts it as a
27 * principal ("Invalid principal, arn: ..." surfaced as a typed synthetic
28 * error via the distilled patch). Explicitly typed for the same
29 * declaration-emit reason as above.
30 */
31export const retryWhileInvalidPrincipal = <A, E extends { _tag: string }, R>(
32 self: Effect.Effect<A, E, R>,
33): Effect.Effect<A, E, R> =>
34 Effect.retry(self, {
35 while: (e) => e._tag === "InvalidLakeFormationPrincipal",
36 schedule: Schedule.max([Schedule.fixed("3 seconds"), Schedule.recurs(10)]),
37 });
38
39/**
40 * Observe the permissions a specific principal holds on a resource.
41 *
42 * IMPORTANT: this deliberately lists by `Resource` only and filters entries
43 * client-side by exact principal identifier. Listing with a `Principal`
44 * filter makes Lake Formation fold the `IAM_ALLOWED_PRINCIPALS` group grant
45 * (typically `ALL`) into the principal's effective permissions, which would
46 * make the reconciler believe `ALL` was granted directly (verified live).
47 */
48export const observePrincipalPermissions = Effect.fn(
49 "AWS.LakeFormation.observePrincipalPermissions",
50)(function* (
51 principal: string,
52 resource: lf.Resource,
53 catalogId: string | undefined,
54) {
55 const pages = yield* lf.listPermissions
56 .pages({ CatalogId: catalogId, Resource: resource })
57 .pipe(Stream.runCollect);
58 const entries = Array.from(pages)
59 .flatMap((page) => page.PrincipalResourcePermissions ?? [])
60 .filter((e) => e.Principal?.DataLakePrincipalIdentifier === principal);
61 const permissions = [
62 ...new Set(entries.flatMap((e) => e.Permissions ?? [])),
63 ].sort();
64 const permissionsWithGrantOption = [
65 ...new Set(entries.flatMap((e) => e.PermissionsWithGrantOption ?? [])),
66 ].sort();
67 return { permissions, permissionsWithGrantOption };
68});