leistungsbilanz-ts/tests/security/roles.test.ts
Grovy311 a7a1595cbf Add test and security baseline
- make quick/check/mutation from cj/security-baseline (Makefile unchanged,
  settings in baseline.mk), pre-commit, gitleaks, Semgrep rules, ESLint
  security rules, Forgejo workflow
- tests/security: generic baseline tests plus app config and role tests
  against createApp() with an in-memory database
- extract createApp() from src/server/index.ts; LB_DATABASE_PATH for tests
- fix gaps found by the tests: reject unknown fields on project create,
  answer malformed JSON with 400 instead of 500, set nosniff on the API
- npm audit fix (next 16.4.1 and others, no high/critical left)
- clear ESLint errors, safeFileName without control-character regex
- per-project ownership stays an open, documented gap (idorGap -> TODO)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 09:40:33 +02:00

88 lines
3.6 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* App-spezifische Sicherheitstests (Leistungsbilanz): die Rollen-Grenze admin/user und die
* Pflicht zum Passwortwechsel – über echte HTTP-Requests gegen createApp().
* Ergänzt baseline.test.ts, das nur Login-Pflicht und Objekt-Zugriff kennt.
*/
import assert from "node:assert/strict";
import type { AddressInfo } from "node:net";
import { after, before, describe, it } from "node:test";
import config, { START_PASSWORD, WRITE_HEADER, signIn } from "./security.config.ts";
type Headers = Record<string, string>;
let server: import("node:http").Server;
let baseUrl = "";
let admin: Headers = {};
let user: Headers = {};
let userId = "";
async function call(method: string, path: string, headers: Headers, body?: unknown) {
return fetch(baseUrl + path, {
method,
headers: { ...headers, "content-type": "application/json" },
body: body === undefined ? undefined : JSON.stringify(body),
});
}
before(async () => {
const app = await config.createApp();
const { userService } = await import("../../src/server/composition/auth.js");
await userService.createUser({
username: "chefin-admin",
name: "chefin-admin",
role: "admin",
initialPassword: START_PASSWORD,
});
await userService.createUser({
username: "neu-ohne-wechsel",
name: "neu-ohne-wechsel",
role: "user",
initialPassword: START_PASSWORD,
});
server = app.listen(0, "127.0.0.1");
await new Promise((resolve) => server.once("listening", resolve));
baseUrl = `http://127.0.0.1:${(server.address() as AddressInfo).port}`;
admin = await signIn(baseUrl, "chefin-admin");
user = await signIn(baseUrl, "planer-a");
const list = (await (await call("GET", "/api/users", admin)).json()) as { id: string; username: string }[];
userId = list.find((entry) => entry.username === "planer-b")!.id;
});
after(() => new Promise<void>((resolve) => server.close(() => resolve())));
describe("Rollen: Benutzerverwaltung nur für admin", () => {
it("normaler Nutzer bekommt 403 auf jede /api/users-Route", async () => {
const cases: [string, string, unknown?][] = [
["GET", "/api/users"],
["POST", "/api/users", { username: "eingeschleust", name: "x", role: "admin", initialPassword: "lang-genug-123" }],
["PATCH", `/api/users/${userId}`, { role: "admin" }],
["POST", `/api/users/${userId}/reset-password`, { newPassword: "lang-genug-123" }],
];
const failures: string[] = [];
for (const [method, path, body] of cases) {
const res = await call(method, path, user, body);
if (res.status !== 403) failures.push(`${method} ${path} als user -> ${res.status}`);
}
assert.deepEqual(failures, [], failures.join("\n"));
});
it("admin erreicht die Benutzerverwaltung (Gegenprobe)", async () => {
const res = await call("GET", "/api/users", admin);
assert.equal(res.status, 200);
});
});
describe("Startpasswort", () => {
it("ohne Passwortwechsel ist außer Konto-Endpunkten alles gesperrt", async () => {
const login = await call("POST", "/api/auth/login", WRITE_HEADER, {
username: "neu-ohne-wechsel",
password: START_PASSWORD,
});
assert.equal(login.status, 200);
const cookie = login.headers.getSetCookie().find((c) => c.startsWith("lb_session="))!.split(";")[0];
const headers = { ...WRITE_HEADER, cookie };
assert.equal((await call("GET", "/api/projects", headers)).status, 403);
assert.equal((await call("POST", "/api/projects", headers, { name: "x" })).status, 403);
assert.equal((await call("GET", "/api/auth/me", headers)).status, 200, "Gegenprobe: /auth/me bleibt erreichbar");
});
});