160 lines
6.0 KiB
TypeScript
160 lines
6.0 KiB
TypeScript
import { expect, test, vi } from "vitest";
|
|
import { buildApp } from "../src/app.js";
|
|
import { loadConfig } from "../src/config.js";
|
|
import type { PiManagementService } from "../src/pi/management.js";
|
|
import { piManagementRoutes } from "../src/routes/pi-management.js";
|
|
|
|
void piManagementRoutes;
|
|
|
|
function fakeService(): PiManagementService {
|
|
return {
|
|
status: vi.fn(async () => ({
|
|
version: "0.80.3", ready: true,
|
|
credentials: "present",
|
|
config: { provider: "zai", model: "glm-5.2", reasoning: "medium" },
|
|
checkedAt: "2026-08-05T10:00:00.000Z",
|
|
})),
|
|
test: vi.fn(async () => ({ ready: true, checkedAt: "2026-08-05T10:00:00.000Z" })),
|
|
logs: vi.fn(async () => ({ lines: ["Pi smoke check succeeded"], checkedAt: "2026-08-05T10:00:00.000Z" })),
|
|
};
|
|
}
|
|
|
|
function appWith(service: PiManagementService, env: Record<string, string> = {}) {
|
|
return buildApp(loadConfig({ THT_HARNESS_DIR: "../harness", ...env }), {
|
|
thtRunner: {} as any,
|
|
piManagement: service,
|
|
});
|
|
}
|
|
|
|
const adminHeaders = {
|
|
"x-thoth-principal-issuer": "portal",
|
|
"x-thoth-principal-subject": "operator",
|
|
"x-thoth-is-admin": "1",
|
|
};
|
|
|
|
const exposedServerEnv = {
|
|
AUTH_MODE: "upstream",
|
|
THOTH_PUBLIC_EXPOSURE: "true",
|
|
THT_SESSION_STORAGE: "postgres",
|
|
THT_SESSION_DB_HOST: "db.example.invalid",
|
|
THT_SESSION_DB_NAME: "thoth_sessions",
|
|
THT_SESSION_RUNTIME_USER: "thoth_runtime",
|
|
THT_SESSION_RUNTIME_PASSWORD_FILE: "/run/secrets/session-password",
|
|
THT_SESSION_DB_SSLMODE: "verify-full",
|
|
THT_SESSION_DB_SSLROOTCERT: "/run/secrets/session-ca.pem",
|
|
};
|
|
|
|
// Catches a server deployment that lets an ordinary authenticated user inspect or mutate
|
|
// installation-wide Pi configuration without the trusted upstream admin claim.
|
|
test("exposed upstream deployments reject Pi Management without a trusted admin identity", async () => {
|
|
const service = fakeService();
|
|
const app = appWith(service, exposedServerEnv);
|
|
try {
|
|
const response = await app.inject({
|
|
method: "GET", url: "/pi-management/status",
|
|
headers: { ...adminHeaders, "x-thoth-is-admin": "0" },
|
|
});
|
|
|
|
expect(response.statusCode).toBe(403);
|
|
expect(response.json()).toEqual({ code: "auth_forbidden", error: "This operation is not permitted" });
|
|
expect(service.status).not.toHaveBeenCalled();
|
|
} finally {
|
|
await app.close();
|
|
}
|
|
});
|
|
|
|
test("Pi Management test requires pi.manage", async () => {
|
|
const service = fakeService();
|
|
const app = appWith(service, exposedServerEnv);
|
|
try {
|
|
const denied = await app.inject({
|
|
method: "POST", url: "/pi-management/test",
|
|
headers: { ...adminHeaders, "x-thoth-is-admin": "0" },
|
|
});
|
|
const allowed = await app.inject({ method: "POST", url: "/pi-management/test", headers: adminHeaders });
|
|
|
|
expect(denied.statusCode).toBe(403);
|
|
expect(denied.json()).toEqual({ code: "auth_forbidden", error: "This operation is not permitted" });
|
|
expect(allowed.statusCode).toBe(200);
|
|
} finally {
|
|
await app.close();
|
|
}
|
|
});
|
|
|
|
// Catches an accidental privilege regression that blocks safe loopback-only installations or
|
|
// returns fields beyond the sanctioned Pi Management status contract.
|
|
test("loopback-only AUTH_MODE=none may read the sanitized Pi status", async () => {
|
|
const app = appWith(fakeService());
|
|
try {
|
|
const response = await app.inject({ method: "GET", url: "/pi-management/status" });
|
|
|
|
expect(response.statusCode).toBe(200);
|
|
expect(response.json()).toEqual({
|
|
version: "0.80.3", ready: true,
|
|
credentials: "present",
|
|
config: { provider: "zai", model: "glm-5.2", reasoning: "medium" },
|
|
checkedAt: "2026-08-05T10:00:00.000Z",
|
|
});
|
|
} finally {
|
|
await app.close();
|
|
}
|
|
});
|
|
|
|
// A local implicit administrator has pi.manage, but a browser origin still cannot borrow that
|
|
// authority to trigger provider work.
|
|
test("loopback-only management rejects cross-origin writes for its local administrator", async () => {
|
|
const service = fakeService();
|
|
const app = appWith(service);
|
|
try {
|
|
const smoke = await app.inject({
|
|
method: "POST", url: "/pi-management/test",
|
|
headers: { host: "127.0.0.1:8080", origin: "https://evil.example" },
|
|
});
|
|
|
|
expect(smoke.statusCode).toBe(403);
|
|
expect(service.test).not.toHaveBeenCalled();
|
|
} finally {
|
|
await app.close();
|
|
}
|
|
});
|
|
|
|
// Catches an origin guard that also blocks the same-origin Docker frontend or non-browser local
|
|
// lifecycle clients that do not send Origin.
|
|
test("loopback-only management preserves same-origin and origin-less smoke checks", async () => {
|
|
const service = fakeService();
|
|
const app = appWith(service);
|
|
try {
|
|
const sameOrigin = await app.inject({
|
|
method: "POST", url: "/pi-management/test",
|
|
headers: { host: "127.0.0.1:8080", origin: "http://127.0.0.1:8080" },
|
|
});
|
|
const lifecycleClient = await app.inject({ method: "POST", url: "/pi-management/test" });
|
|
|
|
expect(sameOrigin.statusCode).toBe(200);
|
|
expect(lifecycleClient.statusCode).toBe(200);
|
|
} finally {
|
|
await app.close();
|
|
}
|
|
});
|
|
|
|
// Catches a regression that reintroduces a browser-writable provider/model source.
|
|
test("trusted admins receive only status, smoke, and log endpoints", async () => {
|
|
const service = fakeService();
|
|
const app = appWith(service, exposedServerEnv);
|
|
try {
|
|
const status = await app.inject({ method: "GET", url: "/pi-management/status", headers: adminHeaders });
|
|
const smoke = await app.inject({ method: "POST", url: "/pi-management/test", headers: adminHeaders });
|
|
const logs = await app.inject({ method: "GET", url: "/pi-management/logs", headers: adminHeaders });
|
|
|
|
expect(status.statusCode).toBe(200);
|
|
expect(smoke.statusCode).toBe(200);
|
|
expect(logs.statusCode).toBe(200);
|
|
expect((await app.inject({ method: "GET", url: "/pi-management/options", headers: adminHeaders })).statusCode).toBe(404);
|
|
expect((await app.inject({ method: "PUT", url: "/pi-management/config", headers: adminHeaders })).statusCode).toBe(404);
|
|
expect(app.printRoutes()).not.toContain("update");
|
|
expect(app.printRoutes()).not.toContain("rollback");
|
|
} finally {
|
|
await app.close();
|
|
}
|
|
});
|