1import * as Effect from "effect/Effect";
2import * as Binding from "../../Binding.ts";
3import * as Output from "../../Output.ts";
4import type { Output as OutputType } from "../../Output.ts";
5import { isBindingHost } from "../Lambda/Function.ts";
6import type { DataCatalog } from "./DataCatalog.ts";
7import type { WorkGroup } from "./WorkGroup.ts";
8
9/**
10 * Shared scaffolding for Athena HTTP bindings.
11 *
12 * NOT exported from `index.ts` — every thin `{Op}Http.ts` in this service is
13 * a `Layer.effect(Cap, make…HttpBinding({ … }))` over one of the two builders
14 * below. Everything except the operation, the IAM action list, and the
15 * injected identifier is boilerplate.
16 */
17
18/**
19 * Build the impl Effect for a workgroup-scoped query operation. Athena
20 * authorizes query-execution actions against the workgroup the query ran in,
21 * so the deploy-time half grants `actions` on the bound {@link WorkGroup}'s
22 * ARN. When `injectWorkGroup` is set, the runtime callable also injects the
23 * workgroup's name as the request's `WorkGroup` field (for operations like
24 * `ListQueryExecutions` that scope by workgroup in the request itself).
25 */
26export const makeWorkGroupScopedHttpBinding = <I, A, E, R>(options: {
27 /** Fully-qualified binding tag, e.g. `AWS.Athena.GetQueryExecution`. */
28 tag: string;
29 /** The distilled operation. */
30 operation: Effect.Effect<(input: I) => Effect.Effect<A, E>, never, R>;
31 /** IAM actions granted on the workgroup ARN. */
32 actions: readonly string[];
33 /** Inject the bound workgroup's name as the request `WorkGroup` field. */
34 injectWorkGroup?: boolean;
35}) =>
36 Effect.gen(function* () {
37 const op = yield* options.operation;
38
39 return Effect.fn(function* (workGroup: WorkGroup) {
40 const WorkGroupName = yield* workGroup.workGroupName;
41 if (!globalThis.__ALCHEMY_RUNTIME__) {
42 const host = yield* Binding.Host;
43 if (isBindingHost(host)) {
44 yield* host.bind`Allow(${host}, ${options.tag}(${workGroup}))`({
45 policyStatements: [
46 {
47 Effect: "Allow",
48 Action: [...options.actions],
49 Resource: [workGroup.workGroupArn],
50 },
51 ],
52 });
53 }
54 }
55 return Effect.fn(`${options.tag}(${workGroup.LogicalId})`)(function* (
56 request: Omit<I, "WorkGroup">,
57 ) {
58 return yield* op(
59 (options.injectWorkGroup
60 ? { ...request, WorkGroup: yield* WorkGroupName }
61 : request) as I,
62 );
63 });
64 });
65 });
66
67// The datacatalog ARN is `arn:aws:athena:{region}:{account}:datacatalog/{name}`
68// — the Glue Data Catalog ARNs share that partition/region/account.
69const glueArns = (dataCatalogArn: OutputType<string>) => {
70 const base = dataCatalogArn.pipe(
71 Output.map((arn) => {
72 const [, partition, , region, account] = arn.split(":");
73 return `arn:${partition}:glue:${region}:${account}`;
74 }),
75 );
76 return [
77 base.pipe(Output.map((b) => `${b}:catalog`)),
78 base.pipe(Output.map((b) => `${b}:database/*`)),
79 base.pipe(Output.map((b) => `${b}:table/*/*`)),
80 ];
81};
82
83/**
84 * Build the impl Effect for a catalog-metadata operation. The runtime callable
85 * injects the bound {@link DataCatalog}'s name as `CatalogName`; the
86 * deploy-time half grants `actions` on the datacatalog ARN plus the Glue
87 * Data Catalog reads Athena performs on the caller's behalf when the catalog
88 * resolves through Glue.
89 */
90export const makeDataCatalogScopedHttpBinding = <
91 I extends { CatalogName: string },
92 A,
93 E,
94 R,
95>(options: {
96 /** Fully-qualified binding tag, e.g. `AWS.Athena.GetDatabase`. */
97 tag: string;
98 /** The distilled operation; `CatalogName` is injected from the resource. */
99 operation: Effect.Effect<(input: I) => Effect.Effect<A, E>, never, R>;
100 /** IAM actions granted on the datacatalog ARN. */
101 actions: readonly string[];
102}) =>
103 Effect.gen(function* () {
104 const op = yield* options.operation;
105
106 return Effect.fn(function* (catalog: DataCatalog) {
107 const CatalogName = yield* catalog.name;
108 if (!globalThis.__ALCHEMY_RUNTIME__) {
109 const host = yield* Binding.Host;
110 if (isBindingHost(host)) {
111 yield* host.bind`Allow(${host}, ${options.tag}(${catalog}))`({
112 policyStatements: [
113 {
114 Effect: "Allow",
115 Action: [...options.actions],
116 Resource: [catalog.dataCatalogArn],
117 },
118 {
119 // Metadata reads through a GLUE-type catalog federate to the
120 // Glue Data Catalog with the caller's credentials.
121 Effect: "Allow",
122 Action: [
123 "glue:GetDatabase",
124 "glue:GetDatabases",
125 "glue:GetTable",
126 "glue:GetTables",
127 "glue:GetPartition",
128 "glue:GetPartitions",
129 ],
130 Resource: glueArns(catalog.dataCatalogArn),
131 },
132 ],
133 });
134 }
135 }
136 return Effect.fn(`${options.tag}(${catalog.LogicalId})`)(function* (
137 request: Omit<I, "CatalogName">,
138 ) {
139 return yield* op({
140 ...request,
141 CatalogName: yield* CatalogName,
142 } as I);
143 });
144 });
145 });