128 lines
4.9 KiB
TypeScript
128 lines
4.9 KiB
TypeScript
import { test, expect } from "vitest";
|
|
import Fastify from "fastify";
|
|
import { authPreHandler, getPrincipal } from "../src/auth/auth.js";
|
|
import { chmodSync, mkdtempSync, readFileSync, rmSync, statSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { expandLocalHome, localPrincipal, upstreamPrincipal } from "../src/auth/principal.js";
|
|
|
|
test("server smoke trusted claims transform through nginx to a non-admin principal", () => {
|
|
const smoke = readFileSync("../scripts/unified-deployment-smoke.sh", "utf8");
|
|
const nginx = readFileSync("../docker/nginx.conf.template", "utf8");
|
|
const helper = smoke.match(/task13_server_auth_headers\(\) \{([\s\S]*?)\n\}/)?.[1] ?? "";
|
|
const trusted = Object.fromEntries(
|
|
[...helper.matchAll(/-H '([^:']+): ([^']+)'/g)].map((match) => [match[1].toLowerCase(), match[2]]),
|
|
);
|
|
const normalized: Record<string, string> = {};
|
|
for (const [header, suffix] of [
|
|
["x-thoth-principal-issuer", "principal_issuer"],
|
|
["x-thoth-principal-subject", "principal_subject"],
|
|
["x-thoth-principal-display-name", "principal_display_name"],
|
|
["x-thoth-is-admin", "is_admin"],
|
|
]) {
|
|
expect(nginx).toContain(`$http_x_thoth_trusted_${suffix}`);
|
|
const value = trusted[`x-thoth-trusted-${header.slice("x-thoth-".length)}`];
|
|
if (value !== undefined) normalized[header] = value;
|
|
}
|
|
|
|
expect(upstreamPrincipal(normalized)).toEqual({
|
|
issuer: "task13-proxy",
|
|
subject: "task13-user",
|
|
displayName: "Task 13 User",
|
|
roles: ["user"],
|
|
permissions: ["session.use"],
|
|
isAdmin: false,
|
|
});
|
|
});
|
|
|
|
test("local mode resolves a stable local principal", async () => {
|
|
const app = Fastify();
|
|
app.addHook("preHandler", authPreHandler("none"));
|
|
app.get("/me", async (req) => getPrincipal(req));
|
|
expect((await app.inject({ method: "GET", url: "/me" })).json()).toMatchObject({
|
|
issuer: "local",
|
|
subject: expect.any(String),
|
|
roles: ["admin"],
|
|
permissions: [
|
|
"session.use", "session.read_all", "session.manage_all", "settings.manage",
|
|
"workspace.manage", "workspace.secrets.manage", "pi.manage", "auth.diagnostics.read",
|
|
],
|
|
isAdmin: true,
|
|
});
|
|
});
|
|
|
|
test("mock mode makes a principal from the test header", async () => {
|
|
const app = Fastify();
|
|
app.addHook("preHandler", authPreHandler("mock"));
|
|
app.get("/me", async (req) => getPrincipal(req));
|
|
const res = await app.inject({
|
|
method: "GET",
|
|
url: "/me",
|
|
headers: { "x-mock-user": "alice" },
|
|
});
|
|
expect(res.json()).toEqual({
|
|
issuer: "mock", subject: "alice", displayName: "alice",
|
|
roles: ["user"], permissions: ["session.use"], isAdmin: false,
|
|
});
|
|
});
|
|
|
|
test("upstream mode accepts only normalized proxy principal headers", async () => {
|
|
const app = Fastify();
|
|
app.addHook("preHandler", authPreHandler("upstream"));
|
|
app.get("/me", async (req) => getPrincipal(req));
|
|
|
|
expect((await app.inject({ method: "GET", url: "/me" })).statusCode).toBe(401);
|
|
const authenticated = await app.inject({
|
|
method: "GET",
|
|
url: "/me",
|
|
headers: {
|
|
"x-thoth-principal-issuer": "portal",
|
|
"x-thoth-principal-subject": "42",
|
|
"x-thoth-principal-display-name": "Alice",
|
|
"x-thoth-is-admin": "1",
|
|
"x-authenticated-user": "must-not-be-used",
|
|
},
|
|
});
|
|
expect(authenticated.json()).toEqual({
|
|
issuer: "portal", subject: "42", displayName: "Alice", roles: ["user", "admin"],
|
|
permissions: [
|
|
"session.use", "session.read_all", "session.manage_all", "settings.manage",
|
|
"workspace.manage", "workspace.secrets.manage", "pi.manage", "auth.diagnostics.read",
|
|
],
|
|
isAdmin: true,
|
|
});
|
|
});
|
|
|
|
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");
|
|
const home = mkdtempSync(join(tmpdir(), "thoth-principal-"));
|
|
chmodSync(home, 0o755);
|
|
const previous = process.env.THT_HOME;
|
|
process.env.THT_HOME = home;
|
|
try {
|
|
localPrincipal();
|
|
if (process.platform !== "win32") {
|
|
expect(statSync(home).mode & 0o777).toBe(0o700);
|
|
expect(statSync(join(home, "identity.json")).mode & 0o777).toBe(0o600);
|
|
}
|
|
} finally {
|
|
if (previous === undefined) delete process.env.THT_HOME; else process.env.THT_HOME = previous;
|
|
rmSync(home, { recursive: true, force: true });
|
|
}
|
|
});
|