import { describe, expect, test } from "bun:test"; import { cleanupExpiredSessions, createApp } from "../../server/app"; import { hashToken, MemoryAuthStore } from "../../server/auth"; import { MemoryAuditStore } from "../../server/audit-store"; import { MemoryOperationStore } from "../../server/operation-store"; import type { ManagementService } from "../../server/management"; import { MemoryTrustStore } from "../../server/trust-store"; import { MemoryWorkspaceStore } from "../../server/workspace-store"; const now = Date.parse("2026-09-05T00:00:00.000Z"); function request(path: string, init: RequestInit = {}, token = "admin-token") { const headers = new Headers(init.headers); headers.set("authorization", `Bearer ${token}`); return new Request(`https://kuber.astrxl.dev${path}`, { ...init, headers }); } async function setup() { const store = new MemoryAuthStore(); const auditStore = new MemoryAuditStore(() => new Date(now)); const operationStore = new MemoryOperationStore(() => new Date(now)); await store.putUser({ username: "admin", passwordHash: "hash", roles: ["admin"], }); await store.putUser({ username: "ci", passwordHash: "hash", roles: ["operator"], }); await store.putSession({ tokenHash: hashToken("admin-token"), username: "admin", authVersion: 1, expiresAt: "2026-10-05T00:00:00.000Z", }); return { store, auditStore, operationStore, app: createApp({ store, auditStore, operationStore, now: () => now }), }; } describe("API keys", () => { test("creates once, lists without a token or hash, and revokes", async () => { const { app, auditStore } = await setup(); const created = await app( request("/api/v2/users/ci/keys", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ capabilities: ["kubernetes:read"], workspace: "shop", }), }), ); expect(created.status).toBe(201); const key = (await created.json()) as { id: string; token: string }; expect(key.token).toHaveLength(43); const listed = await app(request("/api/v2/users/ci/keys")); const body = JSON.stringify(await listed.json()); expect(body).not.toContain(key.token); expect(body).not.toContain(hashToken(key.token)); expect(JSON.stringify(await auditStore.list())).not.toContain(key.token); expect(JSON.stringify(await auditStore.list())).not.toContain( hashToken(key.token), ); expect( ( await app( request(`/api/v2/users/ci/keys/${key.id}`, { method: "DELETE" }), ) ).status, ).toBe(204); expect((await app(request("/api/v2/me", {}, key.token))).status).toBe(401); }); test("uses key capabilities rather than owner roles and invalidates disabled owners", async () => { const { app, store } = await setup(); await store.createApiKey({ id: "key_capability_test", tokenHash: hashToken("ci-key"), username: "ci", capabilities: ["kubernetes:read"], expiresAt: "2026-10-05T00:00:00.000Z", }); expect((await app(request("/api/v2/users", {}, "ci-key"))).status).toBe( 403, ); expect((await app(request("/api/v2/me", {}, "ci-key"))).status).toBe(200); await store.updateUser("ci", { disabled: true }); expect((await app(request("/api/v2/me", {}, "ci-key"))).status).toBe(401); }); test("limits API key children to the parent's user, capabilities, and workspace", async () => { const { app, store, auditStore } = await setup(); await store.createApiKey({ id: "key_delegation_parent", tokenHash: hashToken("delegation-parent"), username: "ci", capabilities: ["users:write", "kubernetes:read"], workspace: "shop", expiresAt: "2026-10-05T00:00:00.000Z", }); const create = (body: unknown) => app( request( "/api/v2/users/ci/keys", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body), }, "delegation-parent", ), ); const capabilities = await create({ capabilities: ["kubernetes:write"], workspace: "shop", }); expect(capabilities.status).toBe(403); const capabilityError = (await capabilities.json()) as { code: string }; expect(capabilityError.code).toBe("API_KEY_DELEGATION_FORBIDDEN"); const workspace = await create({ capabilities: ["kubernetes:read"] }); expect(workspace.status).toBe(403); const differentWorkspace = await create({ capabilities: ["kubernetes:read"], workspace: "other", }); expect(differentWorkspace.status).toBe(403); const subset = await create({ capabilities: ["kubernetes:read"], workspace: "shop", }); expect(subset.status).toBe(201); expect( ((await subset.json()) as { capabilities: string[] }).capabilities, ).toEqual(["kubernetes:read"]); const otherUser = await app( request( "/api/v2/users/admin/keys", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ capabilities: ["kubernetes:read"], workspace: "shop", }), }, "delegation-parent", ), ); expect(otherUser.status).toBe(403); const denied = await auditStore.list(); expect( denied.filter( (event) => event.spec.action === "api_key.create" && event.spec.outcome === "denied", ), ).toHaveLength(4); expect(JSON.stringify(denied)).not.toContain("delegation-parent"); await store.createApiKey({ id: "key_unscoped_delegation", tokenHash: hashToken("unscoped-delegation"), username: "ci", capabilities: ["users:write", "kubernetes:read"], expiresAt: "2026-10-05T00:00:00.000Z", }); const unscopedSubset = await app( request( "/api/v2/users/ci/keys", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ capabilities: ["kubernetes:read"] }), }, "unscoped-delegation", ), ); expect(unscopedSubset.status).toBe(201); expect(await unscopedSubset.json()).not.toHaveProperty("workspace"); }); test("rejects expired keys and expiry longer than 365 days", async () => { const { app, store } = await setup(); await store.createApiKey({ id: "key_expiry_test_1", tokenHash: hashToken("expired-key"), username: "ci", capabilities: ["kubernetes:read"], expiresAt: "2026-09-04T00:00:00.000Z", }); expect((await app(request("/api/v2/me", {}, "expired-key"))).status).toBe( 401, ); expect( ( await app( request("/api/v2/users/ci/keys", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ capabilities: ["kubernetes:read"], expiresAt: "2027-09-06T00:00:00.000Z", }), }), ) ).status, ).toBe(400); }); test("uses the default expiry, cleans up expired keys, and does not log keys out", async () => { const { app, store } = await setup(); const created = await app( request("/api/v2/users/ci/keys", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ capabilities: ["kubernetes:read"] }), }), ); const key = (await created.json()) as { token: string; expiresAt: string }; expect(key.expiresAt).toBe("2026-12-04T00:00:00.000Z"); expect( (await app(request("/api/v2/logout", { method: "POST" }, key.token))) .status, ).toBe(204); expect((await app(request("/api/v2/me", {}, key.token))).status).toBe(200); expect(await cleanupExpiredSessions(store, Date.parse(key.expiresAt))).toBe( 2, ); expect((await app(request("/api/v2/me", {}, key.token))).status).toBe(401); }); test("denies a workspace-scoped key outside its workspace", async () => { const { app, store } = await setup(); await store.createApiKey({ id: "key_scope_test_1", tokenHash: hashToken("scoped-key"), username: "ci", capabilities: ["kubernetes:read"], workspace: "shop", expiresAt: "2026-10-05T00:00:00.000Z", }); expect( (await app(request("/api/v2/workspaces/other", {}, "scoped-key"))).status, ).toBe(403); }); test("allows scoped keys to apply only in their workspace and rejects deleted owners", async () => { const { store } = await setup(); const workspaceStore = new MemoryWorkspaceStore({ uid: () => "workspace-uid", }); for (const id of ["shop", "other"]) await workspaceStore.create({ id, source: { uri: `oci://example/${id}`, digest: "sha256:abc" }, }); const trustStore = new MemoryTrustStore(); const fingerprint = "a".repeat(64); await trustStore.grant("shop", fingerprint); let applies = 0; const app = createApp({ store, workspaceStore, trustStore, operationStore: new MemoryOperationStore(() => new Date(now)), management: { applyResources: async () => { applies += 1; return []; }, } as unknown as ManagementService, now: () => now, }); await store.createApiKey({ id: "key_apply_scope_1", tokenHash: hashToken("scoped-apply-key"), username: "ci", capabilities: ["kubernetes:write"], workspace: "shop", expiresAt: "2026-10-05T00:00:00.000Z", }); const apply = (workspace: string) => app( request( `/api/v2/workspaces/${workspace}/resources/apply`, { method: "POST", headers: { "idempotency-key": `apply-${workspace}`, "x-kuber-trust-project": "shop", "x-kuber-trust-fingerprint": fingerprint, }, body: JSON.stringify({ resources: [] }), }, "scoped-apply-key", ), ); expect((await apply("shop")).status).toBe(200); expect((await apply("other")).status).toBe(403); expect(applies).toBe(1); await store.deleteUser("ci"); expect( (await app(request("/api/v2/me", {}, "scoped-apply-key"))).status, ).toBe(401); }); test("limits workspace-scoped keys to their own audit records", async () => { const { app, store, auditStore } = await setup(); await store.createApiKey({ id: "key_audit_scope_1", tokenHash: hashToken("scoped-audit-key"), username: "ci", capabilities: ["users:read"], workspace: "shop", expiresAt: "2026-10-05T00:00:00.000Z", }); await auditStore.append({ actor: { username: "admin" }, action: "workspace.shop", workspaceId: "shop", outcome: "success", }); await auditStore.append({ actor: { username: "admin" }, action: "workspace.other", workspaceId: "other", outcome: "success", }); await auditStore.append({ actor: { username: "admin" }, action: "platform.global", outcome: "success", }); const unfiltered = await app( request("/api/v2/audit", {}, "scoped-audit-key"), ); expect(unfiltered.status).toBe(200); const audit = (await unfiltered.json()) as { items: { spec: { action: string } }[]; }; expect(audit.items.map((event) => event.spec.action)).toEqual([ "workspace.shop", ]); expect( ( await app( request("/api/v2/audit?workspaceId=other", {}, "scoped-audit-key"), ) ).status, ).toBe(403); }); test("automatically scopes unfiltered operation lists for workspace keys", async () => { const { app, store, operationStore } = await setup(); await operationStore.create({ workspaceId: "shop", action: "resources.apply", idempotencyKey: "shop-operation", }); await operationStore.create({ workspaceId: "other", action: "resources.apply", idempotencyKey: "other-operation", }); await store.createApiKey({ id: "key_operation_scope_1", tokenHash: hashToken("scoped-operation-key"), username: "ci", capabilities: ["kubernetes:read"], workspace: "shop", expiresAt: "2026-10-05T00:00:00.000Z", }); const unfiltered = await app( request("/api/v2/operations", {}, "scoped-operation-key"), ); expect(unfiltered.status).toBe(200); expect( ( (await unfiltered.json()) as { items: { spec: { workspaceId: string } }[]; } ).items.map((operation) => operation.spec.workspaceId), ).toEqual(["shop"]); expect( ( await app( request( "/api/v2/operations?workspaceId=shop", {}, "scoped-operation-key", ), ) ).status, ).toBe(200); expect( ( await app( request( "/api/v2/operations?workspaceId=other", {}, "scoped-operation-key", ), ) ).status, ).toBe(403); }); test("does not inherit an admin owner's platform adoption privilege", async () => { const { store } = await setup(); const adopted: string[] = []; const app = createApp({ store, adoption: { adopt: async () => ({ workspaceId: "", workspaceUid: "", resourcesAdopted: 0, }), adoptPlatform: async (workspaceUid) => { adopted.push(workspaceUid); return { workspaceId: "kuber-system", workspaceUid, resourcesAdopted: 1, }; }, }, now: () => now, }); await store.createApiKey({ id: "key_platform_owner", tokenHash: hashToken("admin-owner-key"), username: "admin", capabilities: ["kubernetes:write"], workspace: "kuber-system", expiresAt: "2026-10-05T00:00:00.000Z", }); await store.createApiKey({ id: "key_platform_scope", tokenHash: hashToken("wrong-scope-key"), username: "admin", capabilities: ["platform:adopt"], workspace: "shop", expiresAt: "2026-10-05T00:00:00.000Z", }); await store.createApiKey({ id: "key_platform_allowed", tokenHash: hashToken("platform-key"), username: "admin", capabilities: ["platform:adopt"], workspace: "kuber-system", expiresAt: "2026-10-05T00:00:00.000Z", }); const adopt = (token: string) => app( request( "/api/v2/platform/kuber-system/adopt", { method: "POST", body: JSON.stringify({ workspaceUid: "platform" }), }, token, ), ); expect((await adopt("admin-owner-key")).status).toBe(403); expect((await adopt("wrong-scope-key")).status).toBe(403); expect((await adopt("platform-key")).status).toBe(200); expect(adopted).toEqual(["platform"]); }); test("allows a workspace-scoped key to use matching project build routes", async () => { const { app, store } = await setup(); await store.createApiKey({ id: "key_scope_build_1", tokenHash: hashToken("scoped-build-key"), username: "ci", capabilities: ["kubernetes:write"], workspace: "shop", expiresAt: "2026-10-05T00:00:00.000Z", }); const matching = await app( request( "/api/v2/images/resolve", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ project: "shop", service: "web" }), }, "scoped-build-key", ), ); expect(matching.status).toBe(503); expect( ( await app( request( "/api/v2/images/resolve", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ project: "other", service: "web" }), }, "scoped-build-key", ), ) ).status, ).toBe(403); }); });