1import * as GCP from "@/GCP";
2import * as Test from "@/Test/Alchemy";
3import * as acm from "@distilled.cloud/gcp/accesscontextmanager_v1";
4import { expect } from "alchemy-test";
5import * as Effect from "effect/Effect";
6import * as Schedule from "effect/Schedule";
7import { logLevel, projectContext, runLifecycle } from "./common.ts";
8
9const { test } = Test.make({ providers: GCP.providers() });
10
11const GROUP_KEY = "01d520gv4vjcrht";
12
13const waitUntilGone = (name: string) =>
14 acm.getOrganizationsGcpUserAccessBindings({ name }).pipe(
15 Effect.as("found" as const),
16 Effect.catchTag("NotFound", () => Effect.succeed("gone" as const)),
17 Effect.repeat({
18 schedule: Schedule.spaced("2 seconds"),
19 until: (status) => status === "gone",
20 times: 10,
21 }),
22 );
23
24test.provider(
25 "getOrganizationsGcpUserAccessBindings on an inaccessible binding fails with Forbidden",
26 (stack) =>
27 Effect.gen(function* () {
28 yield* stack.destroy();
29
30 const ctx = yield* projectContext();
31 const organization = ctx.organization ?? "organizations/123456789012";
32 const error = yield* Effect.flip(
33 acm.getOrganizationsGcpUserAccessBindings({
34 name: `${organization}/gcpUserAccessBindings/alchemy-missing-binding`,
35 }),
36 );
37 expect(error._tag).toEqual("Forbidden");
38
39 yield* stack.destroy();
40 }).pipe(logLevel),
41 {
42 tags: ["provider:gcp", "provider:gcp:accesscontextmanager", "live"],
43 timeout: 90_000,
44 },
45);
46
47test.provider.skipIf(!runLifecycle)(
48 "create, update, and delete a gcp user access binding",
49 (stack) =>
50 Effect.gen(function* () {
51 yield* stack.destroy();
52
53 const ctx = yield* projectContext();
54 const scopes =
55 ctx.projectNumber.length > 0
56 ? [`projects/${ctx.projectNumber}`]
57 : undefined;
58
59 const created = yield* stack.deploy(
60 Effect.gen(function* () {
61 const policy = yield* GCP.AccessContextManager.AccessPolicy(
62 "BindingPolicy",
63 {
64 title: "user access binding policy",
65 scopes,
66 },
67 );
68 const level =
69 yield* GCP.AccessContextManager.AccessPoliciesAccessLevel(
70 "Trusted",
71 {
72 policy: policy.name,
73 title: "trusted devices",
74 basic: { conditions: [{ regions: ["US"] }] },
75 },
76 );
77 const binding = yield* GCP.AccessContextManager.GcpUserAccessBinding(
78 "Engineers",
79 {
80 organization: ctx.organization,
81 groupKey: GROUP_KEY,
82 accessLevels: [level.name],
83 },
84 );
85 return { policy, level, binding };
86 }),
87 );
88
89 expect(created.binding.name).toContain("/gcpUserAccessBindings/");
90 expect(created.binding.groupKey).toEqual(GROUP_KEY);
91 expect(created.binding.accessLevels).toContain(created.level.name);
92
93 const fetched = yield* acm.getOrganizationsGcpUserAccessBindings({
94 name: created.binding.name,
95 });
96 expect(fetched.name).toEqual(created.binding.name);
97 expect(fetched.groupKey).toEqual(GROUP_KEY);
98
99 const updated = yield* stack.deploy(
100 Effect.gen(function* () {
101 const policy = yield* GCP.AccessContextManager.AccessPolicy(
102 "BindingPolicy",
103 {
104 title: "user access binding policy",
105 scopes,
106 },
107 );
108 const level =
109 yield* GCP.AccessContextManager.AccessPoliciesAccessLevel(
110 "Trusted",
111 {
112 policy: policy.name,
113 accessLevelId: created.level.accessLevelId,
114 title: "trusted devices",
115 basic: { conditions: [{ regions: ["US"] }] },
116 },
117 );
118 const binding = yield* GCP.AccessContextManager.GcpUserAccessBinding(
119 "Engineers",
120 {
121 organization: ctx.organization,
122 groupKey: GROUP_KEY,
123 accessLevels: [level.name],
124 dryRunAccessLevels: [level.name],
125 },
126 );
127 return { policy, level, binding };
128 }),
129 );
130
131 expect(updated.binding.name).toEqual(created.binding.name);
132 expect(updated.binding.dryRunAccessLevels).toContain(created.level.name);
133
134 yield* stack.destroy();
135
136 const gone = yield* waitUntilGone(created.binding.name);
137 expect(gone).toEqual("gone");
138 }).pipe(logLevel),
139 {
140 tags: ["provider:gcp", "provider:gcp:accesscontextmanager", "live"],
141 timeout: 120_000,
142 },
143);