import { createHash } from "node:crypto"; import { describe, expect, test } from "bun:test"; import type { KubernetesObject, KubernetesObjectApi, } from "@kubernetes/client-node"; import { hashToken } from "../../server/auth"; import { KubernetesAuthStore, KUBER_SYSTEM_NAMESPACE, } from "../../server/kubernetes-store"; type StoredSecret = KubernetesObject & { data?: Record; stringData?: Record; type?: string; }; function objectName(prefix: string, value: string): string { const digest = createHash("sha256").update(value).digest("hex").slice(0, 48); return `${prefix}-${digest}`; } function encode(value: string): string { return Buffer.from(value).toString("base64"); } function secret( recordType: "user" | "session" | "api-key", name: string, values: Record, ): StoredSecret { return { apiVersion: "v1", kind: "Secret", metadata: { name, namespace: KUBER_SYSTEM_NAMESPACE, labels: { "kuber.astrxl.dev/type": recordType }, }, type: "Opaque", data: Object.fromEntries( Object.entries(values).map(([key, value]) => [key, encode(value)]), ), }; } class FakeObjects { readonly secrets = new Map(); readonly patches: StoredSecret[] = []; readonly deleted: string[] = []; reads = 0; async read(value: KubernetesObject): Promise { this.reads += 1; const found = this.secrets.get(value.metadata?.name ?? ""); if (!found) throw { code: 404 }; return found; } async patch(value: StoredSecret): Promise { const stored: StoredSecret = { ...value, data: Object.fromEntries( Object.entries(value.stringData ?? {}).map(([key, item]) => [ key, encode(item), ]), ), }; delete stored.stringData; this.patches.push(value); this.secrets.set(value.metadata?.name ?? "", stored); return stored; } async list( _apiVersion: string, _kind: string, _namespace: string, _pretty?: string, _exact?: boolean, _export?: boolean, _fieldSelector?: string, labelSelector?: string, ): Promise<{ items: StoredSecret[] }> { const type = labelSelector?.split("=")[1]; return { items: [...this.secrets.values()].filter( (item) => item.metadata?.labels?.["kuber.astrxl.dev/type"] === type, ), }; } async delete(value: KubernetesObject): Promise { const name = value.metadata?.name ?? ""; if (!this.secrets.delete(name)) throw { code: 404 }; this.deleted.push(name); } } function setup(): { fake: FakeObjects; store: KubernetesAuthStore; } { const fake = new FakeObjects(); return { fake, store: new KubernetesAuthStore(fake as unknown as KubernetesObjectApi), }; } describe("KubernetesAuthStore", () => { test("persists absent expiry, reads finite legacy keys, and rejects malformed expiries", async () => { const { fake, store } = setup(); await store.putUser({ username: "alice", passwordHash: "hash", roles: ["viewer"], }); const never = { id: "never-expiring-key-123", tokenHash: hashToken("never"), username: "alice", capabilities: ["kubernetes:read" as const], }; await store.createApiKey(never); expect(fake.patches.at(-1)?.stringData).not.toHaveProperty("expiresAt"); expect(await store.getApiKey(never.tokenHash)).toMatchObject(never); const finite = { ...never, id: "finite-expiry-key-123", tokenHash: hashToken("finite"), expiresAt: "2020-01-01T00:00:00.000Z", }; await store.createApiKey(finite); expect( (await store.listApiKeys("alice")).map((key) => key.expiresAt), ).toEqual([finite.expiresAt, undefined]); expect(await store.deleteExpiredApiKeys()).toBe(1); expect(await store.getApiKey(never.tokenHash)).toMatchObject(never); const active = { ...finite, id: "active-finite-key-123", tokenHash: hashToken("active-finite"), expiresAt: new Date(Date.now() + 60_000).toISOString(), }; await store.createApiKey(active); expect(await store.getApiKey(active.tokenHash)).toMatchObject(active); const name = objectName("api-key", never.tokenHash); for (const expiry of ["none", "", "2026-09-03"]) { fake.secrets.set( name, secret("api-key", name, { id: never.id, tokenHash: never.tokenHash, username: never.username, capabilities: JSON.stringify(never.capabilities), workspace: "", disabled: "false", expiresAt: expiry, }), ); expect(await store.getApiKey(never.tokenHash)).toBeUndefined(); } expect(await store.revokeApiKey("bob", finite.id)).toBe(false); await expect( store.createApiKey({ ...never, username: "missing" }), ).rejects.toThrow("not active"); await store.updateUser("alice", { disabled: true }); await expect(store.createApiKey(never)).rejects.toThrow("not active"); }); test("validates the session and user from the store on every request", async () => { const { fake, store } = setup(); const tokenHash = hashToken("fresh"); await store.putUser({ username: "alice", passwordHash: "hash", roles: ["viewer"], }); await store.putSession({ tokenHash, username: "alice", roles: ["viewer"], expiresAt: "2026-09-03T00:00:00.000Z", }); fake.reads = 0; await expect(store.getSession(tokenHash)).resolves.toMatchObject({ tokenHash, }); fake.secrets.set( objectName("user", "alice"), secret("user", objectName("user", "alice"), { username: "alice", passwordHash: "hash", roles: JSON.stringify(["viewer"]), authVersion: "1", disabled: "true", }), ); await expect(store.getSession(tokenHash)).resolves.toBeUndefined(); expect(fake.reads).toBe(4); }); test("persists authVersion and not copied roles in new sessions", async () => { const { fake, store } = setup(); await store.putUser({ username: "alice", passwordHash: "hash", roles: ["viewer"], }); const tokenHash = hashToken("token"); await store.putSession({ tokenHash, username: "alice", roles: ["admin"], expiresAt: "2026-09-03T00:00:00.000Z", }); expect(fake.patches[0]?.stringData).toMatchObject({ authVersion: "1" }); expect(fake.patches[1]?.stringData).toEqual({ tokenHash, username: "alice", authVersion: "1", expiresAt: "2026-09-03T00:00:00.000Z", }); expect(await store.getSession(tokenHash)).toMatchObject({ authVersion: 1 }); }); test("reads legacy records at version one and invalidates them on update", async () => { const { fake, store } = setup(); const tokenHash = hashToken("legacy"); fake.secrets.set( objectName("user", "alice"), secret("user", objectName("user", "alice"), { username: "alice", passwordHash: "hash", roles: JSON.stringify(["viewer"]), disabled: "false", }), ); fake.secrets.set( objectName("session", tokenHash), secret("session", objectName("session", tokenHash), { tokenHash, username: "alice", roles: JSON.stringify(["admin"]), expiresAt: "2026-09-03T00:00:00.000Z", }), ); expect((await store.getUser("alice"))?.authVersion).toBe(1); expect((await store.getSession(tokenHash))?.authVersion).toBe(1); expect( (await store.updateUser("alice", { roles: ["operator"] }))?.authVersion, ).toBe(2); expect(await store.getSession(tokenHash)).toBeUndefined(); }); test("fails closed for malformed, mislabeled, and swapped Secret data", async () => { const { fake, store } = setup(); const name = objectName("user", "alice"); const valid = secret("user", name, { username: "alice", passwordHash: "hash", roles: JSON.stringify(["viewer"]), authVersion: "1", disabled: "false", }); fake.secrets.set(name, { ...valid, data: { ...valid.data, roles: "%%%" } }); expect(await store.getUser("alice")).toBeUndefined(); fake.secrets.set(name, { ...valid, type: "kubernetes.io/tls" }); expect(await store.getUser("alice")).toBeUndefined(); fake.secrets.set(name, { ...valid, data: { ...valid.data, unexpected: encode("value") }, }); expect(await store.getUser("alice")).toBeUndefined(); fake.secrets.set( name, secret("user", name, { username: "bob", passwordHash: "hash", roles: JSON.stringify(["viewer"]), authVersion: "1", disabled: "false", }), ); expect(await store.getUser("alice")).toBeUndefined(); }); test("lists, creates, updates, revokes, and deletes users and sessions", async () => { const { fake, store } = setup(); await store.createUser({ username: "bob", passwordHash: "b", roles: ["viewer"], }); await store.createUser({ username: "alice", passwordHash: "a", roles: ["operator"], }); await expect( store.createUser({ username: "alice", passwordHash: "a", roles: ["viewer"], }), ).rejects.toThrow("already exists"); expect((await store.listUsers()).map((user) => user.username)).toEqual([ "alice", "bob", ]); expect( (await store.updateUser("alice", { disabled: true }))?.authVersion, ).toBe(2); const bobToken = hashToken("bob"); await store.putSession({ tokenHash: bobToken, username: "bob", authVersion: 1, expiresAt: "2026-09-03T00:00:00.000Z", }); expect(await store.deleteUser("bob")).toBe(true); expect(fake.secrets.has(objectName("session", bobToken))).toBe(false); expect(await store.deleteUser("missing")).toBe(false); }); test("lists and deletes expired sessions without a cluster", async () => { const { fake, store } = setup(); await store.putUser({ username: "alice", passwordHash: "hash", roles: ["viewer"], }); for (const [token, expiration] of [ ["expired", "2026-09-01T00:00:00.000Z"], ["active", "2026-09-03T00:00:00.000Z"], ] as const) { await store.putSession({ tokenHash: hashToken(token), username: "alice", authVersion: 1, expiresAt: expiration, }); } const now = Date.parse("2026-09-02T00:00:00.000Z"); expect(await store.listExpiredSessions(now)).toHaveLength(1); expect(await store.deleteExpiredSessions(now)).toBe(1); expect(fake.secrets.has(objectName("session", hashToken("active")))).toBe( true, ); }); });