1import * as Effect from "effect/Effect";
2import * as Binding from "../../Binding.ts";
3import * as Output from "../../Output.ts";
4import { AWSEnvironment } from "../Environment.ts";
5import { isBindingHost } from "../Lambda/Function.ts";
6import type { Instance } from "./Instance.ts";
7
8/**
9 * Shared scaffolding for the IAM Identity Center runtime bindings.
10 *
11 * NOT exported from `index.ts` — every thin `{Op}Http.ts` in this service is
12 * a `Layer.effect(Cap, make…HttpBinding({ … }))` over one of the two builders
13 * below. Everything except the operation and the IAM action list is
14 * boilerplate:
15 *
16 * - {@link makeIdentityStoreHttpBinding} — Identity Store data-plane
17 * operations (`identitystore:*` actions: user CRUD, group lookups,
18 * membership management). The runtime callable injects the bound
19 * {@link Instance}'s `IdentityStoreId`; the deploy-time half grants
20 * `actions` on the identity store ARN plus the region-less
21 * `user/*`/`group/*`/`membership/*` sub-resource ARNs the actions require.
22 * - {@link makeIdentityCenterInstanceHttpBinding} — `sso:*` admin reads
23 * scoped to the bound {@link Instance} (account-assignment audit,
24 * permission set reads). The runtime callable injects the instance's
25 * `InstanceArn`; the deploy-time half grants `actions` on the instance ARN
26 * plus the region-less `permissionSet/*` and `account/*` ARNs.
27 */
28
29/**
30 * Build the impl Effect for an Identity Store data-plane operation scoped to
31 * one {@link Instance}: the deploy-time half grants `actions` on the
32 * identity store ARN (`arn:aws:identitystore::{account}:identitystore/{id}`)
33 * and the `user/*`/`group/*`/`membership/*` sub-resources, and the runtime
34 * half injects the instance's `IdentityStoreId` into every request.
35 */
36export const makeIdentityStoreHttpBinding = <
37 I extends { IdentityStoreId?: string },
38 A,
39 E,
40 R,
41>(options: {
42 /** Fully-qualified binding tag, e.g. `AWS.IdentityCenter.DescribeUser`. */
43 tag: string;
44 /** The distilled operation; `IdentityStoreId` is injected from the instance. */
45 operation: Effect.Effect<(input: I) => Effect.Effect<A, E>, never, R>;
46 /** IAM actions granted on the identity store + sub-resource ARNs. */
47 actions: readonly string[];
48}) =>
49 Effect.gen(function* () {
50 const op = yield* options.operation;
51
52 return Effect.fn(function* (instance: Instance) {
53 const IdentityStoreId = yield* instance.identityStoreId;
54 if (!globalThis.__ALCHEMY_RUNTIME__) {
55 const host = yield* Binding.Host;
56 if (isBindingHost(host)) {
57 const { accountId } =
58 yield* AWSEnvironment.current as unknown as Effect.Effect<{
59 accountId: string;
60 }>;
61 yield* host.bind`Allow(${host}, ${options.tag}(${instance}))`({
62 policyStatements: [
63 {
64 Effect: "Allow",
65 Action: [...options.actions],
66 Resource: [
67 Output.interpolate`arn:aws:identitystore::${accountId}:identitystore/${instance.identityStoreId}`,
68 // Identity Store sub-resources are region-less and
69 // account-less; the concrete user/group/membership ids
70 // are chosen per request at runtime.
71 "arn:aws:identitystore:::user/*",
72 "arn:aws:identitystore:::group/*",
73 "arn:aws:identitystore:::membership/*",
74 ],
75 },
76 ],
77 });
78 }
79 }
80 return Effect.fn(`${options.tag}(${instance.LogicalId})`)(function* (
81 request?: Omit<I, "IdentityStoreId">,
82 ) {
83 return yield* op({
84 ...request,
85 IdentityStoreId: yield* IdentityStoreId,
86 } as I);
87 });
88 });
89 });
90
91/**
92 * Build the impl Effect for an `sso:*` admin operation scoped to one
93 * {@link Instance}: the deploy-time half grants `actions` on the instance
94 * ARN plus the region-less `permissionSet/*` and `account/*` ARNs, and the
95 * runtime half injects the instance's `InstanceArn` into every request.
96 */
97export const makeIdentityCenterInstanceHttpBinding = <
98 I extends { InstanceArn?: string },
99 A,
100 E,
101 R,
102>(options: {
103 /** Fully-qualified binding tag, e.g. `AWS.IdentityCenter.ListPermissionSets`. */
104 tag: string;
105 /** The distilled operation; `InstanceArn` is injected from the instance. */
106 operation: Effect.Effect<(input: I) => Effect.Effect<A, E>, never, R>;
107 /** IAM actions granted on the instance / permission set / account ARNs. */
108 actions: readonly string[];
109}) =>
110 Effect.gen(function* () {
111 const op = yield* options.operation;
112
113 return Effect.fn(function* (instance: Instance) {
114 const InstanceArn = yield* instance.instanceArn;
115 if (!globalThis.__ALCHEMY_RUNTIME__) {
116 const host = yield* Binding.Host;
117 if (isBindingHost(host)) {
118 yield* host.bind`Allow(${host}, ${options.tag}(${instance}))`({
119 policyStatements: [
120 {
121 Effect: "Allow",
122 Action: [...options.actions],
123 Resource: [
124 Output.interpolate`${instance.instanceArn}`,
125 // Permission sets and target accounts are chosen per
126 // request at runtime; their ARNs are region-less and
127 // account-less.
128 "arn:aws:sso:::permissionSet/*",
129 "arn:aws:sso:::account/*",
130 ],
131 },
132 ],
133 });
134 }
135 }
136 return Effect.fn(`${options.tag}(${instance.LogicalId})`)(function* (
137 request?: Omit<I, "InstanceArn">,
138 ) {
139 return yield* op({
140 ...request,
141 InstanceArn: yield* InstanceArn,
142 } as I);
143 });
144 });
145 });