fix(deploy): close container final review
This commit is contained in:
+6
-1
@@ -42,7 +42,12 @@ export function buildApp(config: AppConfig, deps?: BuildAppDeps): FastifyInstanc
|
||||
const listModels = deps?.listModels ?? createPiModelLister(config);
|
||||
const getSettings = deps?.getSettings ?? (() => effectiveSettings(config, loadSettings(config)));
|
||||
|
||||
app.addHook("preHandler", authPreHandler(config.authMode));
|
||||
const authenticate = authPreHandler(config.authMode);
|
||||
app.addHook("preHandler", async (req, reply) => {
|
||||
// Process readiness is intentionally unauthenticated for local container/proxy probes.
|
||||
if (req.url === "/health") return;
|
||||
return authenticate(req, reply);
|
||||
});
|
||||
app.get("/health", async () => ({ status: "ok" }));
|
||||
sessionRoutes(app, {
|
||||
mgr, tht: tht as ThtRunner, hub, getSettings,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { FastifyRequest, FastifyReply } from "fastify";
|
||||
|
||||
export function authPreHandler(mode: "none" | "mock" | "oidc") {
|
||||
export function authPreHandler(mode: "none" | "mock" | "upstream") {
|
||||
return async (req: FastifyRequest, reply: FastifyReply) => {
|
||||
if (mode === "none") {
|
||||
(req as any).user = { id: "dev@local" };
|
||||
@@ -9,8 +9,11 @@ export function authPreHandler(mode: "none" | "mock" | "oidc") {
|
||||
id: (req.headers["x-mock-user"] as string) ?? "mock",
|
||||
};
|
||||
} else {
|
||||
reply.code(501);
|
||||
throw new Error("OIDC non configurato (MVP: usa none/mock)");
|
||||
const id = req.headers["x-authenticated-user"];
|
||||
if (typeof id !== "string" || id.trim() === "") {
|
||||
return reply.code(401).send({ error: "authenticated upstream identity required" });
|
||||
}
|
||||
(req as any).user = { id };
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export interface AppConfig {
|
||||
host: string; port: number; harnessDir: string; thtBin: string; piBin: string;
|
||||
authMode: "none" | "mock" | "oidc";
|
||||
authMode: "none" | "mock" | "upstream";
|
||||
defaults: { provider?: string; model?: string; thinking?: string };
|
||||
maxPiProcesses: number;
|
||||
settingsFile: string;
|
||||
@@ -8,13 +8,20 @@ export interface AppConfig {
|
||||
ollamaEnsureTimeoutMs: number;
|
||||
}
|
||||
export function loadConfig(env: Record<string, string | undefined>): AppConfig {
|
||||
const authMode = env.AUTH_MODE ?? "none";
|
||||
if (!(["none", "mock", "upstream"] as const).includes(authMode as AppConfig["authMode"])) {
|
||||
throw new Error(`unsupported AUTH_MODE=${authMode}; use none, mock, or upstream`);
|
||||
}
|
||||
if (env.THOTH_PUBLIC_EXPOSURE === "true" && authMode !== "upstream") {
|
||||
throw new Error("public exposure requires AUTH_MODE=upstream behind a trusted proxy");
|
||||
}
|
||||
return {
|
||||
host: env.HOST ?? "127.0.0.1",
|
||||
port: Number(env.PORT ?? 8787),
|
||||
harnessDir: env.THT_HARNESS_DIR ?? "../harness",
|
||||
thtBin: env.THT_BIN ?? "tht",
|
||||
piBin: env.PI_BIN ?? "pi",
|
||||
authMode: (env.AUTH_MODE as AppConfig["authMode"]) ?? "none",
|
||||
authMode: authMode as AppConfig["authMode"],
|
||||
defaults: { provider: env.PI_PROVIDER, model: env.PI_MODEL, thinking: env.PI_THINKING },
|
||||
maxPiProcesses: Number(env.MAX_PI_PROCESSES ?? 4),
|
||||
settingsFile: env.SETTINGS_FILE ?? "data/settings.json",
|
||||
|
||||
@@ -22,3 +22,17 @@ test("mode mock legge l'header", async () => {
|
||||
});
|
||||
expect(res.json()).toEqual({ id: "alice" });
|
||||
});
|
||||
|
||||
test("upstream mode requires the authenticated proxy identity header", async () => {
|
||||
const app = Fastify();
|
||||
app.addHook("preHandler", authPreHandler("upstream"));
|
||||
app.get("/me", async (req) => getUser(req));
|
||||
|
||||
expect((await app.inject({ method: "GET", url: "/me" })).statusCode).toBe(401);
|
||||
const authenticated = await app.inject({
|
||||
method: "GET",
|
||||
url: "/me",
|
||||
headers: { "x-authenticated-user": "alice@example.test" },
|
||||
});
|
||||
expect(authenticated.json()).toEqual({ id: "alice@example.test" });
|
||||
});
|
||||
|
||||
@@ -32,3 +32,17 @@ test("loadConfig keeps local development defaults", () => {
|
||||
});
|
||||
expect(loadConfig({}).dataRoot).toBeUndefined();
|
||||
});
|
||||
|
||||
test("loadConfig rejects unauthenticated public exposure", () => {
|
||||
expect(() => loadConfig({
|
||||
THOTH_PUBLIC_EXPOSURE: "true",
|
||||
AUTH_MODE: "none",
|
||||
})).toThrow(/public exposure requires AUTH_MODE=upstream/);
|
||||
});
|
||||
|
||||
test("loadConfig accepts an authenticated upstream trust boundary", () => {
|
||||
expect(loadConfig({
|
||||
THOTH_PUBLIC_EXPOSURE: "true",
|
||||
AUTH_MODE: "upstream",
|
||||
}).authMode).toBe("upstream");
|
||||
});
|
||||
|
||||
@@ -10,6 +10,17 @@ test("GET /health reports process readiness without external services", async ()
|
||||
expect(res.json()).toEqual({ status: "ok" });
|
||||
});
|
||||
|
||||
test("GET /health remains available to container probes in upstream auth mode", async () => {
|
||||
const app = buildApp(loadConfig({
|
||||
THT_HARNESS_DIR: "/tmp/h",
|
||||
AUTH_MODE: "upstream",
|
||||
THOTH_PUBLIC_EXPOSURE: "true",
|
||||
}));
|
||||
const res = await app.inject({ method: "GET", url: "/health" });
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(res.json()).toEqual({ status: "ok" });
|
||||
});
|
||||
|
||||
test("SSE response headers are flushed before the first event", async () => {
|
||||
const app = buildApp(loadConfig({ THT_HARNESS_DIR: "/tmp/h" }));
|
||||
await app.listen({ port: 0, host: "127.0.0.1" });
|
||||
|
||||
Reference in New Issue
Block a user