This commit is contained in:
2026-09-03 11:28:30 +07:00 Unverified
parent 26c2d0f015
commit 82d0fe3e0d
93 changed files with 19237 additions and 1285 deletions
+261
View File
@@ -0,0 +1,261 @@
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<string, string>;
stringData?: Record<string, string>;
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",
name: string,
values: Record<string, string>,
): 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<string, StoredSecret>();
readonly patches: StoredSecret[] = [];
readonly deleted: string[] = [];
async read(value: KubernetesObject): Promise<StoredSecret> {
const found = this.secrets.get(value.metadata?.name ?? "");
if (!found) throw { code: 404 };
return found;
}
async patch(value: StoredSecret): Promise<StoredSecret> {
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<void> {
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 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,
);
});
});