1import * as Effect from "effect/Effect";
2import * as Binding from "../../Binding.ts";
3import * as Output from "../../Output.ts";
4import { isBindingHost } from "../Lambda/Function.ts";
5import type { Environment } from "./Environment.ts";
6
7/**
8 * Shared scaffolding for Amazon MWAA HTTP bindings.
9 *
10 * NOT exported from `index.ts` — every `{Op}Http.ts` in this service is a
11 * thin `Layer.effect(Cap, make…HttpBinding({ … }))` over one of the builders
12 * below. Everything except the operation and the IAM action list is
13 * boilerplate: every MWAA data-plane operation is scoped to one Airflow
14 * {@link Environment}, whose name is injected as `Name`.
15 *
16 * MWAA distinguishes two IAM resource shapes:
17 *
18 * - Environment-scoped actions (`airflow:CreateCliToken`,
19 * `airflow:GetEnvironment`) are granted on the environment ARN
20 * `arn:{partition}:airflow:{region}:{account}:environment/{name}`.
21 * - Airflow-RBAC-role-scoped actions (`airflow:CreateWebLoginToken`,
22 * `airflow:InvokeRestApi`) are granted on the Airflow role ARN
23 * `arn:{partition}:airflow:{region}:{account}:role/{name}/{airflow-role}` —
24 * MWAA maps the calling IAM identity to that Apache Airflow RBAC role.
25 */
26
27/**
28 * Options for Airflow-RBAC-role-scoped bindings ({@link Environment} web
29 * login tokens and Airflow REST API invocation).
30 */
31export interface AirflowRoleOptions {
32 /**
33 * The Apache Airflow RBAC role the IAM grant is scoped to — MWAA maps the
34 * function's identity to this role when it calls the webserver. One of the
35 * default Airflow roles (`Admin`, `Op`, `User`, `Viewer`, `Public`) or a
36 * custom role defined in the environment.
37 * @default "Admin"
38 */
39 airflowRole?: string;
40}
41
42/**
43 * Build the impl Effect for an environment-scoped MWAA operation: the runtime
44 * callable injects the bound {@link Environment}'s name as `Name` and the
45 * deploy-time half grants `actions` on the environment ARN.
46 */
47export const makeMWAAEnvironmentHttpBinding = <
48 I extends { Name: string },
49 A,
50 E,
51 R,
52>(options: {
53 /** Fully-qualified binding tag, e.g. `AWS.MWAA.CreateCliToken`. */
54 tag: string;
55 /** The distilled operation; `Name` is injected from the environment. */
56 operation: Effect.Effect<(input: I) => Effect.Effect<A, E>, never, R>;
57 /** IAM actions granted on the environment ARN. */
58 actions: readonly string[];
59}) =>
60 Effect.gen(function* () {
61 const op = yield* options.operation;
62
63 return Effect.fn(function* (environment: Environment) {
64 const Name = yield* environment.environmentName;
65 if (!globalThis.__ALCHEMY_RUNTIME__) {
66 const host = yield* Binding.Host;
67 if (isBindingHost(host)) {
68 yield* host.bind`Allow(${host}, ${options.tag}(${environment}))`({
69 policyStatements: [
70 {
71 Effect: "Allow",
72 Action: [...options.actions],
73 Resource: [Output.interpolate`${environment.arn}`],
74 },
75 ],
76 });
77 }
78 }
79 return Effect.fn(`${options.tag}(${environment.LogicalId})`)(function* (
80 request?: Omit<I, "Name">,
81 ) {
82 return yield* op({ ...request, Name: yield* Name } as I);
83 });
84 });
85 });
86
87/**
88 * Build the impl Effect for an Airflow-RBAC-role-scoped MWAA operation
89 * (`CreateWebLoginToken`, `InvokeRestApi`): the runtime callable injects the
90 * bound {@link Environment}'s name as `Name` and the deploy-time half grants
91 * `actions` on the Airflow role ARN
92 * `arn:{partition}:airflow:{region}:{account}:role/{name}/{airflowRole}` —
93 * MWAA maps the function's IAM identity to that Apache Airflow RBAC role
94 * (default `Admin`).
95 */
96export const makeMWAAAirflowRoleHttpBinding = <
97 I extends { Name: string },
98 A,
99 E,
100 R,
101>(options: {
102 /** Fully-qualified binding tag, e.g. `AWS.MWAA.InvokeRestApi`. */
103 tag: string;
104 /** The distilled operation; `Name` is injected from the environment. */
105 operation: Effect.Effect<(input: I) => Effect.Effect<A, E>, never, R>;
106 /** IAM actions granted on the Airflow role ARN. */
107 actions: readonly string[];
108}) =>
109 Effect.gen(function* () {
110 const op = yield* options.operation;
111
112 return Effect.fn(function* (
113 environment: Environment,
114 roleOptions?: AirflowRoleOptions,
115 ) {
116 const Name = yield* environment.environmentName;
117 const airflowRole = roleOptions?.airflowRole ?? "Admin";
118 if (!globalThis.__ALCHEMY_RUNTIME__) {
119 const host = yield* Binding.Host;
120 if (isBindingHost(host)) {
121 yield* host.bind`Allow(${host}, ${options.tag}(${environment}, ${airflowRole}))`(
122 {
123 policyStatements: [
124 {
125 Effect: "Allow",
126 Action: [...options.actions],
127 // The environment ARN is
128 // `arn:{partition}:airflow:{region}:{account}:environment/{name}`;
129 // the Airflow RBAC role ARN swaps the resource type and
130 // appends the role.
131 Resource: [
132 environment.arn.pipe(
133 Output.map(
134 (arn) =>
135 `${arn.replace(":environment/", ":role/")}/${airflowRole}`,
136 ),
137 ),
138 ],
139 },
140 ],
141 },
142 );
143 }
144 }
145 return Effect.fn(`${options.tag}(${environment.LogicalId})`)(function* (
146 request?: Omit<I, "Name">,
147 ) {
148 return yield* op({ ...request, Name: yield* Name } as I);
149 });
150 });
151 });