/** * Runbook section 3 — the authentication boundary and a real committed write. * * CAP-005 the data plane refuses unauthenticated and wrong credentials * CAP-006 a quorum-acked write commits end to end * CAP-007 operator and metrics surfaces are unroutable from the internet * * The negative cases are the point. A suite that only proves the happy path has * proved that the endpoint works, not that it is protected — and this endpoint * is on the open internet. */ import { expect, test } from '@playwright/test'; import { recordJson } from '../support/evidence'; import { PUBLIC_BASE_URL, apiKey } from '../support/env'; /** * Entity ids for verification writes live in a reserved band so a probe is * never mistaken for corpus data by someone reading the store later. */ const VERIFICATION_ENTITY_BASE = 999_000_000; /** * Paths published through the ingress allowlist in k8s/cluster/ingress.yaml. * Everything else must be rejected by the gateway before it reaches the app. */ const UNPUBLISHED_PATHS = [ '/cluster/status', '/cluster/members', '/metrics', '/openapi.json', ] as const; test.describe('section 3 — authentication boundary', () => { test('the same path is refused without a credential, refused with a wrong one, and served with the right one', async ({ playwright, }, testInfo) => { const anonymous = await playwright.request.newContext(); const wrong = await playwright.request.newContext({ extraHTTPHeaders: { authorization: 'Bearer definitely-not-the-key' }, }); const authorized = await playwright.request.newContext({ extraHTTPHeaders: { authorization: `Bearer ${apiKey()}` }, }); try { const target = `${PUBLIC_BASE_URL}/search?query=verification&limit=1`; const [anonymousResponse, wrongResponse, authorizedResponse] = [ await anonymous.get(target), await wrong.get(target), await authorized.get(target), ]; const observedStatuses = { noCredential: anonymousResponse.status(), wrongCredential: wrongResponse.status(), correctCredential: authorizedResponse.status(), }; await recordJson(testInfo, 'auth-matrix', observedStatuses); // 401 for both denials. A 200 on the wrong bearer would be the highest // severity finding this suite can produce; a 500 would mean the key was // parsed and then mishandled. expect(observedStatuses.noCredential, 'anonymous read must be refused').toBe(401); expect(observedStatuses.wrongCredential, 'wrong bearer must be refused').toBe(401); expect(observedStatuses.correctCredential, 'correct bearer must be served').toBe(200); // A 200 with an empty body would mean the credential was accepted but the // engine returned nothing — proof of auth, not proof of service. const payload = await authorizedResponse.json(); await recordJson(testInfo, 'authorized-search-shape', { keys: Object.keys(payload as Record), }); expect( Object.keys(payload as Record).length, 'an authorized read must return a real payload', ).toBeGreaterThan(0); } finally { await anonymous.dispose(); await wrong.dispose(); await authorized.dispose(); } }); test('a quorum-acked write is committed, not merely accepted', async ({ playwright, }, testInfo) => { const context = await playwright.request.newContext({ baseURL: PUBLIC_BASE_URL, extraHTTPHeaders: { authorization: `Bearer ${apiKey()}` }, }); try { const entityId = VERIFICATION_ENTITY_BASE + 99; const response = await context.post('/items', { headers: { 'content-type': 'application/json', 'x-tidal-ack': 'quorum' }, data: { entity_id: entityId, metadata: { title: 'deploy verification probe', category: 'verification', }, }, }); const body = await response.text(); await recordJson(testInfo, 'quorum-write', { entityId, status: response.status(), body: body.slice(0, 300), }); // 201 specifically. 202 would mean the write was accepted for later // replication — that is exactly the weaker guarantee this check exists to // rule out, so it must not be tolerated as "close enough". expect( response.status(), 'a quorum-ack write must return 201 (committed by quorum), not 202 (accepted)', ).toBe(201); } finally { await context.dispose(); } }); test('operator and metrics surfaces are rejected by the gateway, not merely unauthorized', async ({ playwright, }, testInfo) => { // Deliberately carrying a VALID credential. If a path answered 401 that // would be tolerable; answering 200 to a valid key on /cluster/status would // mean the allowlist has drifted and the operator surface is public. const context = await playwright.request.newContext({ baseURL: PUBLIC_BASE_URL, extraHTTPHeaders: { authorization: `Bearer ${apiKey()}` }, }); try { const statuses: Record = {}; for (const path of UNPUBLISHED_PATHS) { statuses[path] = (await context.get(path)).status(); } await recordJson(testInfo, 'unpublished-path-statuses', statuses); for (const path of UNPUBLISHED_PATHS) { // 404 means Traefik never routed it. 401 would mean the request reached // the application and only the credential check stopped it — the // gateway allowlist would have drifted. expect( statuses[path], `${path} must be unroutable (404) from the internet, not merely unauthorized`, ).toBe(404); } } finally { await context.dispose(); } }); });