1import * as Effect from "effect/Effect";
2import { isResolved } from "../../Diff.ts";
3import * as Provider from "../../Provider.ts";
4import { Resource } from "../../Resource.ts";
5import {
6 hasIamMembership,
7 revokeIamMembership,
8 updateIamMembership,
9 type GcpIamResourceKind,
10} from "../IamPolicy.ts";
11import type { Providers } from "../Providers.ts";
12
13export type MemberProps = {
14 /**
15 * Kind of resource whose IAM policy receives the member, e.g.
16 * `run.service`, `pubsub.topic`, `storage.bucket`, or `project`.
17 * Changing it replaces the grant.
18 */
19 kind: GcpIamResourceKind;
20 /**
21 * Full resource name (`projects/p/locations/l/services/s`), a bucket name
22 * for `storage.bucket`, or a project id for `project`. Changing it
23 * replaces the grant.
24 */
25 name: string;
26 /** Role to grant, e.g. `roles/run.invoker`. Changing it replaces the grant. */
27 role: string;
28 /**
29 * Principal, e.g. `serviceAccount:x@p.iam.gserviceaccount.com`. A bare
30 * email is treated as a service account. Changing it replaces the grant.
31 */
32 member: string;
33};
34
35/**
36 * One `role` → `member` edge on a single resource's IAM policy — the GCP
37 * analog of a Terraform `google_*_iam_member`. Only this edge is managed:
38 * other members of the role, other roles, and conditional bindings are
39 * left alone. Updates are etag-guarded and retried on concurrent writes.
40 *
41 * Event sources use it to let a push identity invoke its Cloud Run host.
42 *
43 * ### Granting a role
44 * **Example:** Let a service account invoke a Cloud Run service
45 * ```typescript
46 * yield* GCP.IAM.Member("Invoker", {
47 * kind: "run.service",
48 * name: service.name,
49 * role: "roles/run.invoker",
50 * member: Output.interpolate`serviceAccount:${caller.email}`,
51 * });
52 * ```
53 *
54 * @resource
55 * @category IAM
56 */
57export type Member = Resource<
58 "GCP.IAM.Member",
59 MemberProps,
60 MemberProps,
61 never,
62 Providers
63>;
64
65export const Member = Resource<Member>("GCP.IAM.Member");
66
67export const MemberProvider = () =>
68 Provider.succeed(Member, {
69 stables: ["kind", "name", "role", "member"],
70
71 diff: Effect.fn(function* ({ news, olds }) {
72 if (!isResolved(news) || olds === undefined) return undefined;
73 const changed =
74 news.kind !== olds.kind ||
75 news.name !== olds.name ||
76 news.role !== olds.role ||
77 news.member !== olds.member;
78 return changed
79 ? { action: "replace" as const, deleteFirst: false }
80 : undefined;
81 }),
82
83 read: Effect.fn(function* ({ olds, output }) {
84 const props = output ?? olds;
85 if (props === undefined) return undefined;
86 const present = yield* hasIamMembership(props);
87 return present ? { ...props } : undefined;
88 }),
89
90 reconcile: Effect.fn(function* ({ news }) {
91 yield* updateIamMembership({
92 kind: news.kind,
93 name: news.name,
94 member: news.member,
95 add: [news.role],
96 });
97 return { ...news };
98 }),
99
100 delete: Effect.fn(function* ({ output }) {
101 yield* revokeIamMembership({
102 kind: output.kind,
103 name: output.name,
104 member: output.member,
105 roles: [output.role],
106 });
107 }),
108 });