1import * as GCP from "@/GCP";
2import * as Test from "@/Test/Alchemy";
3import * as iam from "@distilled.cloud/gcp/iam_v2";
4import { expect } from "alchemy-test";
5import * as Effect from "effect/Effect";
6import { MinimumLogLevel } from "effect/References";
7import * as Schedule from "effect/Schedule";
8import { GcpEnvironment } from "@/GCP/Environment";
9
10const { test } = Test.make({ providers: GCP.providers() });
11
12const logLevel = Effect.provideService(
13 MinimumLogLevel,
14 process.env.DEBUG ? "Debug" : "Info",
15);
16
17// Needs roles/iam.denyAdmin on the project (GCP_TEST_IAM_DENY_ADMIN=1); Owner
18// does not include it, so create fails with `Forbidden: Permission
19// iam.googleapis.com/denypolicies.create denied on resource ...`.
20const runLifecycle = !!process.env.GCP_TEST_IAM_DENY_ADMIN;
21
22const attachmentOf = (project: string) =>
23 encodeURIComponent(`cloudresourcemanager.googleapis.com/projects/${project}`);
24const missingNameOf = (project: string) =>
25 `policies/${attachmentOf(project)}/denypolicies/alchemy-missing`;
26
27const waitUntilGone = (name: string) =>
28 iam.getPolicies({ name }).pipe(
29 Effect.map((policy) =>
30 policy.deleteTime ? ("gone" as const) : ("found" as const),
31 ),
32 Effect.catchTag("NotFound", () => Effect.succeed("gone" as const)),
33 Effect.repeat({
34 schedule: Schedule.spaced("2 seconds"),
35 until: (status) => status === "gone",
36 times: 8,
37 }),
38 );
39
40test.provider(
41 "getPolicies on a missing deny policy fails with a typed tag",
42 (stack) =>
43 Effect.gen(function* () {
44 const { project } = yield* GcpEnvironment.current;
45 const missingName = missingNameOf(project);
46 yield* stack.destroy();
47
48 const error = yield* Effect.flip(iam.getPolicies({ name: missingName }));
49 expect(error._tag).toEqual("NotFound");
50
51 yield* stack.destroy();
52 }).pipe(logLevel),
53 { tags: ["provider:gcp", "provider:gcp:iam", "live"], timeout: 90_000 },
54);
55
56test.provider.skipIf(runLifecycle)(
57 "createPolicyPolicies without deny-policy access fails with a typed tag",
58 (stack) =>
59 Effect.gen(function* () {
60 const { project } = yield* GcpEnvironment.current;
61 const attachment = attachmentOf(project);
62 yield* stack.destroy();
63
64 const error = yield* Effect.flip(
65 iam.createPolicyPolicies({
66 parent: `policies/${attachment}/denypolicies`,
67 policyId: "alchemy-probe",
68 body: {
69 displayName: "alchemy-probe",
70 rules: [
71 {
72 denyRule: {
73 deniedPermissions: ["iam.googleapis.com/roles.list"],
74 deniedPrincipals: [
75 "principal://goog/subject/alchemy-deny-probe@example.invalid",
76 ],
77 },
78 },
79 ],
80 },
81 }),
82 );
83 expect(error._tag).toEqual("Forbidden");
84 expect(error.message).toContain("denypolicies.create");
85
86 yield* stack.destroy();
87 }).pipe(logLevel),
88 { tags: ["provider:gcp", "provider:gcp:iam", "live"], timeout: 90_000 },
89);
90
91test.provider.skipIf(!runLifecycle)(
92 "create, update, and delete a deny policy",
93 (stack) =>
94 Effect.gen(function* () {
95 yield* stack.destroy();
96
97 const created = yield* stack.deploy(
98 Effect.gen(function* () {
99 return yield* GCP.IAM.Policy("Probe", {
100 displayName: "alchemy-probe",
101 });
102 }),
103 );
104
105 expect(created.name).toContain("/denypolicies/");
106 expect(created.policyId).toEqual(expect.any(String));
107 expect(created.displayName).toEqual("alchemy-probe");
108
109 const fetched = yield* iam.getPolicies({ name: created.name });
110 expect(fetched.name).toEqual(created.name);
111 expect(fetched.annotations?.["alchemy-id"]).toEqual(expect.any(String));
112
113 const updated = yield* stack.deploy(
114 Effect.gen(function* () {
115 return yield* GCP.IAM.Policy("Probe", {
116 policyId: created.policyId,
117 displayName: "alchemy-probe-v2",
118 });
119 }),
120 );
121 expect(updated.name).toEqual(created.name);
122 expect(updated.displayName).toEqual("alchemy-probe-v2");
123
124 yield* stack.destroy();
125
126 const gone = yield* waitUntilGone(created.name);
127 expect(gone).toEqual("gone");
128 }).pipe(logLevel),
129 { tags: ["provider:gcp", "provider:gcp:iam", "live"], timeout: 90_000 },
130);