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>
This commit is contained in:
parent
67476ea59b
commit
a7a1595cbf
38 changed files with 5852 additions and 442 deletions
88
tests/security/roles.test.ts
Normal file
88
tests/security/roles.test.ts
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
/**
|
||||
* 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");
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue