Files
kuber/tests/server/auth.test.ts
T

248 lines
7.7 KiB
TypeScript

import { describe, expect, test } from "bun:test";
import {
MemoryAuthStore,
hashToken,
normalizeApiKey,
tokenHashesEqual,
} from "../../server/auth";
import {
CAPABILITIES,
capabilitiesForRoles,
hasCapability,
} from "../../server/authorization";
const expiresAt = "2026-09-03T00:00:00.000Z";
describe("authorization", () => {
test("grants named capabilities through deny-by-default roles", () => {
expect([...capabilitiesForRoles(["viewer"])]).toEqual(["kubernetes:read"]);
expect(hasCapability(["operator"], "kubernetes:write")).toBe(true);
expect(hasCapability(["operator"], "users:read")).toBe(false);
expect([...capabilitiesForRoles(["unknown"])]).toEqual([]);
expect([...capabilitiesForRoles(["admin"])]).toEqual([...CAPABILITIES]);
});
});
describe("memory auth store", () => {
test("keeps non-expiring keys active and preserves finite expiry validation", async () => {
const store = new MemoryAuthStore();
await store.putUser({
username: "alice",
passwordHash: "hash",
roles: ["viewer"],
});
const key = {
id: "never-expiring-key-123",
tokenHash: hashToken("never"),
username: "alice",
capabilities: ["kubernetes:read" as const],
};
await store.createApiKey(key);
expect(await store.getApiKey(key.tokenHash)).toMatchObject(key);
expect((await store.listApiKeys("alice"))[0]?.expiresAt).toBeUndefined();
await store.createApiKey({
...key,
id: "finite-expiry-key-123",
tokenHash: hashToken("finite"),
expiresAt: "2020-01-01T00:00:00.000Z",
});
expect(await store.deleteExpiredApiKeys()).toBe(1);
expect(await store.getApiKey(key.tokenHash)).toMatchObject(key);
expect(
normalizeApiKey({ ...key, expiresAt: undefined }).expiresAt,
).toBeUndefined();
for (const expiresAt of ["none", "not-a-date", "2026-09-03", ""]) {
expect(() => normalizeApiKey({ ...key, expiresAt })).toThrow(
"ISO timestamp",
);
}
await expect(
store.createApiKey({
...key,
id: "duplicate-key-id-123",
tokenHash: hashToken("duplicate"),
}),
).resolves.toBeUndefined();
expect(await store.revokeApiKey("bob", key.id)).toBe(false);
expect(await store.revokeApiKey("alice", key.id)).toBe(true);
expect(await store.getApiKey(key.tokenHash)).toBeUndefined();
await expect(
store.createApiKey({ ...key, username: "missing" }),
).rejects.toThrow("not active");
await store.updateUser("alice", { disabled: true });
await expect(store.createApiKey({ ...key })).rejects.toThrow("not active");
});
test("maintains the API-key hash index across key mutations", async () => {
const store = new MemoryAuthStore();
await store.putUser({
username: "alice",
passwordHash: "hash",
roles: ["viewer"],
});
const activeHash = hashToken("active-api-key");
const expiredHash = hashToken("expired-api-key");
const activeKey = {
id: "active-key-id-1234",
tokenHash: activeHash,
username: "alice",
capabilities: ["kubernetes:read" as (typeof CAPABILITIES)[number]],
expiresAt: new Date(Date.now() + 60_000).toISOString(),
};
await store.createApiKey(activeKey);
expect(store.apiKeysByTokenHash.get(activeHash)).toBe(
store.apiKeys.get(activeKey.id),
);
expect(await store.getApiKey(activeHash)).toEqual(
store.apiKeys.get(activeKey.id),
);
expect(await store.revokeApiKey("alice", activeKey.id)).toBe(true);
expect(store.apiKeysByTokenHash.has(activeHash)).toBe(false);
expect(await store.getApiKey(activeHash)).toBeUndefined();
const expiredKey = {
...activeKey,
id: "expired-key-id-1234",
tokenHash: expiredHash,
expiresAt: "2020-01-01T00:00:00.000Z",
};
await store.createApiKey(expiredKey);
expect(store.apiKeysByTokenHash.get(expiredHash)).toBe(
store.apiKeys.get(expiredKey.id),
);
expect(await store.deleteExpiredApiKeys()).toBe(1);
expect(store.apiKeysByTokenHash.has(expiredHash)).toBe(false);
await store.createApiKey(activeKey);
expect(await store.deleteUser("alice")).toBe(true);
expect(store.apiKeysByTokenHash.has(activeHash)).toBe(false);
expect(await store.getApiKey(activeHash)).toBeUndefined();
});
test("creates, lists, updates, and deletes users", async () => {
const store = new MemoryAuthStore();
const bob = await store.createUser({
username: "bob",
passwordHash: "hash-b",
roles: ["viewer"],
});
await store.createUser({
username: "alice",
passwordHash: "hash-a",
roles: ["operator"],
});
expect(bob.authVersion).toBe(1);
expect((await store.listUsers()).map((user) => user.username)).toEqual([
"alice",
"bob",
]);
await expect(store.createUser({ ...bob })).rejects.toThrow(
"User already exists",
);
const updated = await store.updateUser("bob", {
roles: ["admin"],
disabled: true,
});
expect(updated).toMatchObject({
roles: ["admin"],
disabled: true,
authVersion: 2,
});
expect(await store.updateUser("missing", {})).toBeUndefined();
expect(await store.deleteUser("bob")).toBe(true);
expect(await store.deleteUser("bob")).toBe(false);
});
test("references user authVersion and ignores legacy role snapshots", async () => {
const store = new MemoryAuthStore();
await store.putUser({
username: "alice",
passwordHash: "hash",
roles: ["viewer"],
});
const tokenHash = hashToken("token");
await store.putSession({
tokenHash,
username: "alice",
roles: ["admin"],
expiresAt,
});
expect(store.sessions.get(tokenHash)).toEqual({
tokenHash,
username: "alice",
authVersion: 1,
expiresAt,
});
await store.updateUser("alice", { roles: ["operator"] });
expect(await store.getSession(tokenHash)).toBeUndefined();
await expect(
store.putSession({
tokenHash: hashToken("stale"),
username: "alice",
authVersion: 1,
expiresAt,
}),
).rejects.toThrow("stale");
});
test("revokes user sessions and cleans up expired sessions", async () => {
const store = new MemoryAuthStore();
for (const username of ["alice", "bob"]) {
await store.putUser({
username,
passwordHash: "hash",
roles: ["viewer"],
});
}
await store.putSession({
tokenHash: hashToken("alice-expired"),
username: "alice",
authVersion: 1,
expiresAt: "2026-09-01T00:00:00.000Z",
});
await store.putSession({
tokenHash: hashToken("alice-active"),
username: "alice",
authVersion: 1,
expiresAt,
});
await store.putSession({
tokenHash: hashToken("bob-expired"),
username: "bob",
authVersion: 1,
expiresAt: "2026-09-01T00:00:00.000Z",
});
expect(
await store.listExpiredSessions(Date.parse("2026-09-02T00:00:00.000Z")),
).toHaveLength(2);
expect(
await store.deleteExpiredSessions(Date.parse("2026-09-02T00:00:00.000Z")),
).toBe(2);
expect(await store.revokeUserSessions("alice")).toBe(1);
expect(store.sessions.size).toBe(0);
});
test("rejects invalid domain records and malformed token hashes", async () => {
const store = new MemoryAuthStore();
await expect(
store.putUser({
username: " alice",
passwordHash: "hash",
roles: ["viewer"],
}),
).rejects.toThrow("Username");
await expect(
store.putUser({
username: "alice",
passwordHash: "hash",
roles: ["root"] as never,
}),
).rejects.toThrow("valid role");
expect(tokenHashesEqual("zz", "zz")).toBe(false);
});
});