1import { AWSEnvironment } from "@/AWS/Environment.ts";
2import { Role } from "@/AWS/IAM/Role.ts";
3import * as Lambda from "@/AWS/Lambda";
4import * as SecurityLake from "@/AWS/SecurityLake";
5import * as Effect from "effect/Effect";
6import * as Layer from "effect/Layer";
7import { HttpServerRequest } from "effect/http/HttpServerRequest";
8import * as HttpServerResponse from "effect/http/HttpServerResponse";
9import path from "pathe";
10
11const main = path.resolve(import.meta.dirname, "bindings-handler.ts");
12
13export class SecurityLakeBindingsFunction extends Lambda.Function<Lambda.Function>()(
14 "SecurityLakeBindingsFunction",
15) {}
16
17export default SecurityLakeBindingsFunction.make(
18 {
19 main,
20 functionUrl: true,
21 },
22 Effect.gen(function* () {
23 const { region } = yield* AWSEnvironment.current;
24
25 const metastoreRole = yield* Role("BindingsMetastoreRole", {
26 assumeRolePolicyDocument: {
27 Version: "2012-10-17",
28 Statement: [
29 {
30 Effect: "Allow",
31 Principal: { Service: "lambda.amazonaws.com" },
32 Action: ["sts:AssumeRole"],
33 },
34 ],
35 },
36 managedPolicyArns: [
37 "arn:aws:iam::aws:policy/service-role/AmazonSecurityLakeMetastoreManager",
38 ],
39 });
40
41 const lake = yield* SecurityLake.DataLake("BindingsLake", {
42 configurations: [{ region }],
43 metaStoreManagerRoleArn: metastoreRole.roleArn,
44 });
45
46 const listExceptions = yield* SecurityLake.ListDataLakeExceptions(lake);
47 const getSources = yield* SecurityLake.GetDataLakeSources(lake);
48
49 const bound = { listExceptions, getSources };
50
51 return {
52 fetch: Effect.gen(function* () {
53 const request = yield* HttpServerRequest;
54 const url = new URL(request.originalUrl);
55 const pathname = url.pathname;
56
57 if (request.method === "GET" && pathname === "/bindings") {
58 return yield* HttpServerResponse.json({
59 bound: Object.keys(bound),
60 });
61 }
62
63 if (request.method === "GET" && pathname === "/exceptions") {
64 // An IAM gap would surface AccessDeniedException (a 500); a
65 // successful (possibly empty) list proves the grant end-to-end.
66 const response = yield* listExceptions();
67 return yield* HttpServerResponse.json({
68 count: (response.exceptions ?? []).length,
69 });
70 }
71
72 if (request.method === "GET" && pathname === "/sources") {
73 const response = yield* getSources();
74 return yield* HttpServerResponse.json({
75 dataLakeArn: response.dataLakeArn,
76 count: (response.dataLakeSources ?? []).length,
77 });
78 }
79
80 return yield* HttpServerResponse.json(
81 { error: "Not found", method: request.method, pathname },
82 { status: 404 },
83 );
84 }).pipe(Effect.orDie),
85 };
86 }).pipe(
87 Effect.provide(
88 Layer.mergeAll(
89 SecurityLake.ListDataLakeExceptionsHttp,
90 SecurityLake.GetDataLakeSourcesHttp,
91 ),
92 ),
93 ),
94);