1import * as GCP from "@/GCP";
2import * as Test from "@/Test/Alchemy";
3import * as Core from "@/Test/Core";
4import * as binaryauthorization from "@distilled.cloud/gcp/binaryauthorization_v1";
5import * as resourcemanager from "@distilled.cloud/gcp/cloudresourcemanager_v3";
6import { describe, expect } from "alchemy-test";
7import * as Effect from "effect/Effect";
8import { dockerAvailable, expectProbe } from "../bindingHost.ts";
9import {
10 currentProject,
11 TEST_ATTESTATION,
12 TEST_POD,
13 TEST_RESOURCE_URI,
14} from "./common.ts";
15import BinaryAuthorizationBindingsHost, {
16 GkePolicy,
17 Verifier,
18 Viewed,
19} from "./fixtures/bindings-host.ts";
20
21const testOptions = { providers: GCP.providers() };
22const { test, beforeAll, afterAll } = Test.make(testOptions);
23const sharedStack = Core.scratchStack(
24 testOptions,
25 "BinaryAuthorizationBindings",
26);
27
28let baseUrl: string;
29let hostAccount: string;
30let viewed: { name: string; noteReference: string };
31let verifier: { name: string; noteReference: string };
32let policyName: string;
33
34/** Every project-level role (and its IAM Condition) `account` holds. */
35const projectGrantsOf = (account: string) =>
36 Effect.gen(function* () {
37 const project = yield* currentProject;
38 const policy = yield* resourcemanager.getIamPolicyProjects({
39 resource: `projects/${project}`,
40 body: { options: { requestedPolicyVersion: 3 } },
41 });
42 return (policy.bindings ?? [])
43 .filter((binding) =>
44 (binding.members ?? []).includes(`serviceAccount:${account}`),
45 )
46 .map((binding) => ({
47 role: binding.role,
48 condition: binding.condition?.expression,
49 }))
50 .sort((left, right) => (left.role ?? "").localeCompare(right.role ?? ""));
51 });
52
53/** Platform policies have no resource-level IAM: both roles sit on the project. */
54const PROJECT_GRANTS = [
55 {
56 role: "roles/binaryauthorization.policyEvaluator",
57 condition: undefined,
58 },
59 { role: "roles/binaryauthorization.policyViewer", condition: undefined },
60];
61
62/** Roles `account` holds on an attestor's own IAM policy. */
63const attestorRolesOf = (attestor: string, account: string) =>
64 binaryauthorization
65 .getIamPolicyProjectsAttestors({
66 resource: attestor,
67 "options.requestedPolicyVersion": 3,
68 })
69 .pipe(
70 Effect.map((policy) =>
71 (policy.bindings ?? [])
72 .filter((binding) =>
73 (binding.members ?? []).includes(`serviceAccount:${account}`),
74 )
75 .map((binding) => binding.role),
76 ),
77 );
78
79describe.skipIf(!dockerAvailable)(
80 "BinaryAuthorization Bindings",
81 {
82 tags: [
83 "provider:gcp",
84 "provider:gcp:binaryauthorization",
85 "provider:gcp:run",
86 "live",
87 ],
88 },
89 () => {
90 beforeAll(
91 Effect.gen(function* () {
92 yield* sharedStack.destroy();
93 const out = yield* sharedStack.deploy(
94 Effect.gen(function* () {
95 const host = yield* BinaryAuthorizationBindingsHost;
96 const viewedAttestor = yield* Viewed;
97 const verifierAttestor = yield* Verifier;
98 const policy = yield* GkePolicy;
99 return {
100 uri: host.uri,
101 serviceAccount: host.serviceAccount,
102 viewed: {
103 name: viewedAttestor.name,
104 noteReference: viewedAttestor.noteReference,
105 },
106 verifier: {
107 name: verifierAttestor.name,
108 noteReference: verifierAttestor.noteReference,
109 },
110 policy: policy.name,
111 };
112 }),
113 );
114 baseUrl = out.uri!;
115 hostAccount = out.serviceAccount!;
116 viewed = out.viewed;
117 verifier = out.verifier;
118 policyName = out.policy;
119 }),
120 { timeout: 900_000 },
121 );
122
123 afterAll(sharedStack.destroy(), { timeout: 600_000 });
124
125 describe("GetAttestor", () => {
126 test.provider(
127 "reads the attestor as the host's service account, granted on the attestor only",
128 (_stack) =>
129 Effect.gen(function* () {
130 const live = yield* expectProbe<{
131 name?: string;
132 userOwnedGrafeasNote?: { noteReference?: string };
133 }>(baseUrl, "getAttestor");
134 expect(live.name).toEqual(viewed.name);
135 expect(live.userOwnedGrafeasNote?.noteReference).toEqual(
136 viewed.noteReference,
137 );
138
139 expect(yield* attestorRolesOf(viewed.name, hostAccount)).toEqual([
140 "roles/binaryauthorization.attestorsViewer",
141 ]);
142 expect(yield* projectGrantsOf(hostAccount)).toEqual(PROJECT_GRANTS);
143 }),
144 {
145 tags: ["provider:gcp", "provider:gcp:binaryauthorization", "live"],
146 timeout: 600_000,
147 },
148 );
149 });
150
151 describe("ValidateAttestation", () => {
152 test.provider(
153 "validates against the attestor as the host's service account, granted on the attestor only",
154 (_stack) =>
155 Effect.gen(function* () {
156 const result = yield* expectProbe<{ result?: string }>(
157 baseUrl,
158 "validateAttestation",
159 );
160 // The attestor has no public keys, so nothing can verify.
161 expect(result.result).toEqual("ATTESTATION_NOT_VERIFIABLE");
162 const expected =
163 yield* binaryauthorization.validateAttestationOccurrenceProjectsAttestors(
164 {
165 attestor: verifier.name,
166 body: {
167 occurrenceResourceUri: TEST_RESOURCE_URI,
168 occurrenceNote: verifier.noteReference,
169 attestation: TEST_ATTESTATION,
170 },
171 },
172 );
173 expect(result.result).toEqual(expected.result);
174
175 expect(yield* attestorRolesOf(verifier.name, hostAccount)).toEqual([
176 "roles/binaryauthorization.attestorsVerifier",
177 ]);
178 expect(yield* projectGrantsOf(hostAccount)).toEqual(PROJECT_GRANTS);
179 }),
180 {
181 tags: ["provider:gcp", "provider:gcp:binaryauthorization", "live"],
182 timeout: 600_000,
183 },
184 );
185 });
186
187 describe("GetPlatformsPolicy", () => {
188 test.provider(
189 "reads the platform policy as the host's service account",
190 (_stack) =>
191 Effect.gen(function* () {
192 const live = yield* expectProbe<{ name?: string; etag?: string }>(
193 baseUrl,
194 "getPlatformsPolicy",
195 );
196 const expected =
197 yield* binaryauthorization.getProjectsPlatformsPolicies({
198 name: policyName,
199 });
200 expect(live.name).toEqual(policyName);
201 expect(live.etag).toEqual(expected.etag);
202 expect(yield* projectGrantsOf(hostAccount)).toEqual(PROJECT_GRANTS);
203 }),
204 {
205 tags: ["provider:gcp", "provider:gcp:binaryauthorization", "live"],
206 timeout: 600_000,
207 },
208 );
209 });
210
211 describe("EvaluateGkePolicy", () => {
212 test.provider(
213 "evaluates a pod against the platform policy as the host's service account",
214 (_stack) =>
215 Effect.gen(function* () {
216 const live = yield* expectProbe<{ verdict?: string }>(
217 baseUrl,
218 "evaluateGkePolicy",
219 );
220 const expected =
221 yield* binaryauthorization.evaluateProjectsPlatformsGkePolicies({
222 name: policyName,
223 body: { resource: TEST_POD },
224 });
225 expect(live.verdict).toEqual(expected.verdict);
226 expect(live.verdict).toEqual(expect.any(String));
227 expect(yield* projectGrantsOf(hostAccount)).toEqual(PROJECT_GRANTS);
228 }),
229 {
230 tags: ["provider:gcp", "provider:gcp:binaryauthorization", "live"],
231 timeout: 600_000,
232 },
233 );
234 });
235 },
236);