1import * as GCP from "@/GCP";
2import * as Test from "@/Test/Alchemy";
3import * as networksecurity from "@distilled.cloud/gcp/networksecurity_v1";
4import * as resourcemanager from "@distilled.cloud/gcp/cloudresourcemanager_v3";
5import { expect } from "alchemy-test";
6import * as Effect from "effect/Effect";
7import { MinimumLogLevel } from "effect/References";
8import * as Schedule from "effect/Schedule";
9import { GcpEnvironment } from "@/GCP/Environment";
10
11const { test } = Test.make({ providers: GCP.providers() });
12
13const logLevel = Effect.provideService(
14 MinimumLogLevel,
15 process.env.DEBUG ? "Debug" : "Info",
16);
17
18const waitUntilGone = (name: string) =>
19 networksecurity.getOrganizationsLocationsSecurityProfileGroups({ name }).pipe(
20 Effect.as("found" as const),
21 Effect.catchTag("NotFound", () => Effect.succeed("gone" as const)),
22 Effect.repeat({
23 schedule: Schedule.spaced("1 second"),
24 until: (status) => status === "gone",
25 times: 10,
26 }),
27 );
28
29test.provider(
30 "getOrganizationsLocationsSecurityProfileGroups on a missing group fails with NotFound (Forbidden without org IAM)",
31 (stack) =>
32 Effect.gen(function* () {
33 const { project } = yield* GcpEnvironment.current;
34 yield* stack.destroy();
35
36 const resource = yield* resourcemanager.getProjects({
37 name: `projects/${project}`,
38 });
39 const parent = resource.parent ?? "organizations/0";
40 const organization = parent.startsWith("organizations/")
41 ? parent.slice("organizations/".length)
42 : "0";
43 const error = yield* Effect.flip(
44 networksecurity.getOrganizationsLocationsSecurityProfileGroups({
45 name: `organizations/${organization}/locations/global/securityProfileGroups/alchemy-missing-secprofilegroup`,
46 }),
47 );
48 expect(error._tag).toEqual(
49 process.env.GCP_TEST_ORG_NETWORKSECURITY ? "NotFound" : "Forbidden",
50 );
51
52 yield* stack.destroy();
53 }).pipe(logLevel),
54 {
55 tags: ["provider:gcp", "provider:gcp:networksecurity", "live"],
56 timeout: 90_000,
57 },
58);
59
60// The testing credentials lack org-level Network Security IAM (Forbidden:
61// "Permission 'networksecurity.….get' denied on 'organizations/…'"). Set
62// GCP_TEST_ORG_NETWORKSECURITY=1 when they administer the organization.
63test.provider.skipIf(!!process.env.GCP_TEST_ORG_NETWORKSECURITY)(
64 "createOrganizationsLocationsSecurityProfileGroups without org IAM fails with Forbidden",
65 (stack) =>
66 Effect.gen(function* () {
67 const { project } = yield* GcpEnvironment.current;
68 yield* stack.destroy();
69
70 const resource = yield* resourcemanager.getProjects({
71 name: `projects/${project}`,
72 });
73 const parent = resource.parent ?? "organizations/0";
74 const organization = parent.startsWith("organizations/")
75 ? parent.slice("organizations/".length)
76 : "0";
77 const error = yield* Effect.flip(
78 networksecurity.createOrganizationsLocationsSecurityProfileGroups({
79 parent: `organizations/${organization}/locations/global`,
80 securityProfileGroupId: "alchemy-org-secprofilegroup-probe",
81 body: { description: "probe" },
82 }),
83 );
84 expect(error._tag).toBe("Forbidden");
85
86 yield* stack.destroy();
87 }).pipe(logLevel),
88 {
89 tags: ["provider:gcp", "provider:gcp:networksecurity", "live"],
90 timeout: 90_000,
91 },
92);
93
94test.provider.skipIf(
95 !!process.env.FAST || !process.env.GCP_TEST_ORG_NETWORKSECURITY,
96)(
97 "create, update, and delete an organization security profile group",
98 (stack) =>
99 Effect.gen(function* () {
100 yield* stack.destroy();
101
102 const created = yield* stack.deploy(
103 Effect.gen(function* () {
104 const profile =
105 yield* GCP.NetworkSecurity.OrganizationsSecurityProfile("Threats", {
106 type: "THREAT_PREVENTION",
107 threatPreventionProfile: {
108 severityOverrides: [
109 { severity: "INFORMATIONAL", action: "ALERT" },
110 ],
111 },
112 labels: { env: "test" },
113 });
114 const group =
115 yield* GCP.NetworkSecurity.OrganizationsSecurityProfileGroup(
116 "Profiles",
117 {
118 threatPreventionProfile: profile.name.as<string>(),
119 description: "group a",
120 labels: { env: "test" },
121 },
122 );
123 return { profile, group };
124 }),
125 );
126
127 expect(created.group.name).toContain("/securityProfileGroups/");
128 expect(created.group.name).toContain("organizations/");
129 expect(created.group.threatPreventionProfile).toEqual(
130 created.profile.name,
131 );
132 expect(created.group.labels).toMatchObject({ env: "test" });
133
134 const fetched =
135 yield* networksecurity.getOrganizationsLocationsSecurityProfileGroups({
136 name: created.group.name,
137 });
138 expect(fetched.name).toEqual(created.group.name);
139 expect(fetched.threatPreventionProfile).toEqual(created.profile.name);
140
141 const updated = yield* stack.deploy(
142 Effect.gen(function* () {
143 const profile =
144 yield* GCP.NetworkSecurity.OrganizationsSecurityProfile("Threats", {
145 securityProfileId: created.profile.securityProfileId,
146 organization: created.profile.organization,
147 type: "THREAT_PREVENTION",
148 threatPreventionProfile: {
149 severityOverrides: [
150 { severity: "INFORMATIONAL", action: "ALERT" },
151 ],
152 },
153 labels: { env: "prod" },
154 });
155 const group =
156 yield* GCP.NetworkSecurity.OrganizationsSecurityProfileGroup(
157 "Profiles",
158 {
159 securityProfileGroupId: created.group.securityProfileGroupId,
160 organization: created.group.organization,
161 threatPreventionProfile: profile.name.as<string>(),
162 description: "group b",
163 labels: { env: "prod", role: "ngfw" },
164 },
165 );
166 return { profile, group };
167 }),
168 );
169
170 expect(updated.group.name).toEqual(created.group.name);
171 expect(updated.group.description).toEqual("group b");
172 expect(updated.group.labels).toMatchObject({ env: "prod", role: "ngfw" });
173
174 yield* stack.destroy();
175
176 const gone = yield* waitUntilGone(created.group.name);
177 expect(gone).toEqual("gone");
178 }).pipe(logLevel),
179 {
180 tags: ["provider:gcp", "provider:gcp:networksecurity", "live"],
181 timeout: 120_000,
182 },
183);