| 1 | import * as Effect from "effect/Effect"; |
| 2 | import * as Binding from "../../Binding.ts"; |
| 3 | import * as Output from "../../Output.ts"; |
| 4 | import { AWSEnvironment } from "../Environment.ts"; |
| 5 | import { isBindingHost } from "../Lambda/Function.ts"; |
| 6 | import 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 | */ |
| 36 | export 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 | */ |
| 97 | export 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 | }); |