test: cover user-owned session security boundaries

This commit is contained in:
User
2026-07-16 19:16:58 +02:00
parent 7a65fc80a2
commit ccb3cf4aa9
5 changed files with 160 additions and 2 deletions
+14
View File
@@ -51,6 +51,20 @@ test("upstream mode accepts only normalized proxy principal headers", async () =
});
});
test("upstream mode rejects legacy client identity headers without proxy principal fields", async () => {
const app = Fastify();
app.addHook("preHandler", authPreHandler("upstream"));
app.get("/me", async (req) => getPrincipal(req));
for (const headers of [
{ "x-authenticated-user": "mallory" },
{ "x-mock-user": "mallory" },
{ "x-authenticated-user": "mallory", "x-mock-user": "mallory" },
]) {
expect((await app.inject({ method: "GET", url: "/me", headers })).statusCode).toBe(401);
}
});
test("local identity expands tilde homes and restores private POSIX permissions", () => {
expect(expandLocalHome("~/thoth-test", "/home/tester")).toBe("/home/tester/thoth-test");
expect(expandLocalHome("~", "/home/tester")).toBe("/home/tester");
+53
View File
@@ -87,6 +87,41 @@ test("session listing permits all scope only to admins", async () => {
expect(seen).toEqual([false, true]);
});
test("A, B, and admin requests preserve owner isolation through session route mutations", async () => {
const owners = new Map([["a", "alice"], ["b", "bob"]]);
const closed: Array<{ id: string; subject: string }> = [];
const app = buildApp(loadConfig({ AUTH_MODE: "upstream", THT_HARNESS_DIR: "../harness" }), {
thtRunner: {
withPrincipal: (principal: any) => ({
sessionList: async () => [...owners]
.filter(([, owner]) => principal.isAdmin || owner === principal.subject)
.map(([id, owner]) => ({ id, author: owner })),
sessionShow: async (id: string) =>
(principal.isAdmin || owners.get(id) === principal.subject) ? { id, status: "open" } : null,
closeSession: async (id: string) => { closed.push({ id, subject: principal.subject }); },
}),
} as any,
mgr: { get: () => undefined } as any,
hub: { clear: () => {} } as any,
getSettings: () => ({ workspace: "w" }) as any,
});
const bobHeaders = { ...aliceHeaders, "x-thoth-principal-subject": "bob" };
const adminHeaders = { ...aliceHeaders, "x-thoth-principal-subject": "admin", "x-thoth-is-admin": "1" };
expect((await app.inject({ method: "GET", url: "/sessions", headers: aliceHeaders })).json())
.toEqual([{ id: "a", author: "alice" }]);
expect((await app.inject({ method: "GET", url: "/sessions", headers: bobHeaders })).json())
.toEqual([{ id: "b", author: "bob" }]);
expect((await app.inject({ method: "GET", url: "/sessions?scope=all", headers: adminHeaders })).json())
.toEqual([{ id: "a", author: "alice" }, { id: "b", author: "bob" }]);
expect((await app.inject({ method: "POST", url: "/sessions/a/close", headers: bobHeaders })).statusCode)
.toBe(404);
expect((await app.inject({ method: "POST", url: "/sessions/a/close", headers: adminHeaders })).statusCode)
.toBe(200);
expect(closed).toEqual([{ id: "a", subject: "admin" }]);
});
test("new sessions are created through the authenticated principal, not a client owner field", async () => {
let principal: any;
const app = buildApp(loadConfig({ AUTH_MODE: "upstream", THT_HARNESS_DIR: "../harness" }), {
@@ -1411,6 +1446,24 @@ test("POST /sessions readiness failure returns one fixed public message without
expect(createdCalled).toBe(false);
});
test("POST /sessions returns storage 503 before creating a Pi runtime when session persistence fails", async () => {
let piCreated = false;
const app = buildApp(loadConfig({ THT_HARNESS_DIR: "../harness" }), {
thtRunner: {
ollamaEnsure: async () => ({ ok: true }),
sessionNew: async () => { throw new Error("database unavailable"); },
} as any,
getSettings: () => ({ workspace: "psd" }) as any,
mgr: { createFor: () => { piCreated = true; throw new Error("must not spawn"); } } as any,
});
const response = await app.inject({ method: "POST", url: "/sessions", payload: { question: "q" } });
expect(response.statusCode).toBe(503);
expect(response.json()).toEqual({ error: "session storage is unavailable" });
expect(piCreated).toBe(false);
});
test("POST /sessions proceeds when ollamaEnsure succeeds", async () => {
let ensureWs: string | undefined;
const app = buildApp(loadConfig({ THT_HARNESS_DIR: "../harness" }), {