74 lines
2.7 KiB
JavaScript
74 lines
2.7 KiB
JavaScript
|
|
'use strict';
|
||
|
|
|
||
|
|
// Security invariant: every API router mount is either explicitly public or carries
|
||
|
|
// requireAuth (+ CSRF for authenticated routers). This is a static guard so a new
|
||
|
|
// route added without auth/CSRF fails CI instead of silently exposing data.
|
||
|
|
const test = require('node:test');
|
||
|
|
const assert = require('node:assert/strict');
|
||
|
|
const fs = require('node:fs');
|
||
|
|
const path = require('node:path');
|
||
|
|
|
||
|
|
const server = fs.readFileSync(path.join(__dirname, '..', 'server.cts'), 'utf8');
|
||
|
|
|
||
|
|
// Routers that are intentionally public (or handle their own auth internally).
|
||
|
|
const PUBLIC_ROUTERS = new Set([
|
||
|
|
'auth', // login/register/csrf-token are public; authed sub-routes guard themselves
|
||
|
|
'authOidc', // OIDC login/callback are public entry points
|
||
|
|
'calendarFeed', // token-authenticated ICS feed (no session)
|
||
|
|
'about', // public marketing/info
|
||
|
|
'privacy', // public policy
|
||
|
|
'version', // public version / release-notes / update-status
|
||
|
|
]);
|
||
|
|
|
||
|
|
// Match: app.use('<path>', <middleware...>, require('./routes/<file>.cts'))
|
||
|
|
const mountRe =
|
||
|
|
/app\.use\(\s*['"](\/api\/[^'"]+)['"]([\s\S]*?)require\(\s*['"]\.\/routes\/([A-Za-z0-9_-]+)\.cts['"]\s*\)/g;
|
||
|
|
|
||
|
|
const mounts = [];
|
||
|
|
for (let m; (m = mountRe.exec(server));) {
|
||
|
|
mounts.push({ path: m[1], middleware: m[2], router: m[3] });
|
||
|
|
}
|
||
|
|
|
||
|
|
test('server.cts mounts are discoverable', () => {
|
||
|
|
assert.ok(mounts.length >= 20, `expected many router mounts, found ${mounts.length}`);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('every non-public router mount requires authentication', () => {
|
||
|
|
const offenders = mounts
|
||
|
|
.filter((m) => !PUBLIC_ROUTERS.has(m.router))
|
||
|
|
.filter((m) => !/requireAuth/.test(m.middleware))
|
||
|
|
.map((m) => `${m.path} → routes/${m.router}.cts`);
|
||
|
|
assert.deepEqual(
|
||
|
|
offenders,
|
||
|
|
[],
|
||
|
|
`router mounts missing requireAuth:\n ${offenders.join('\n ')}`,
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('every authenticated router mount enforces CSRF', () => {
|
||
|
|
const offenders = mounts
|
||
|
|
.filter((m) => !PUBLIC_ROUTERS.has(m.router))
|
||
|
|
.filter((m) => !/csrfMiddleware/.test(m.middleware))
|
||
|
|
.map((m) => `${m.path} → routes/${m.router}.cts`);
|
||
|
|
assert.deepEqual(
|
||
|
|
offenders,
|
||
|
|
[],
|
||
|
|
`authed router mounts missing csrfMiddleware:\n ${offenders.join('\n ')}`,
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('admin router mounts require admin', () => {
|
||
|
|
const adminMounts = mounts.filter(
|
||
|
|
(m) => m.path.startsWith('/api/admin') || m.router === 'aboutAdmin' || m.router === 'status',
|
||
|
|
);
|
||
|
|
assert.ok(adminMounts.length >= 1, 'expected at least one admin mount');
|
||
|
|
const offenders = adminMounts
|
||
|
|
.filter((m) => !/requireAdmin/.test(m.middleware))
|
||
|
|
.map((m) => `${m.path} → routes/${m.router}.cts`);
|
||
|
|
assert.deepEqual(
|
||
|
|
offenders,
|
||
|
|
[],
|
||
|
|
`admin mounts missing requireAdmin:\n ${offenders.join('\n ')}`,
|
||
|
|
);
|
||
|
|
});
|