fix(mam-api): narrow cluster carve-out to /cluster/heartbeat only

Code-review feedback: startsWith('/cluster') was a prefix match that exposed
destructive operator endpoints (POST /containers/:id/restart, DELETE /:id,
GET /devices/blackmagic/*) unauthenticated. Only POST /heartbeat is genuine
node-agent traffic; everything else in cluster.js is operator/UI surface
that should go through requireAuth. Long-term: issue node-agent a bound
api_token and drop the carve-out entirely.
This commit is contained in:
Zac Gaetano 2026-05-27 14:18:27 -04:00
parent 9de4fe9ab9
commit cb7cc9a43e

View file

@ -88,13 +88,18 @@ app.use(session({
app.get('/health', (_req, res) => res.json({ status: 'ok' })); app.get('/health', (_req, res) => res.json({ status: 'ok' }));
// ── Auth gate ───────────────────────────────────────────────────────────────── // ── Auth gate ─────────────────────────────────────────────────────────────────
// Mount once for everything under /api/v1, with an explicit allowlist for // req.path is relative to the /api/v1 mount, so /auth/login NOT /api/v1/auth/login.
// the three pre-login auth paths and a carve-out for /cluster/* (node-agent
// uses migration 019's token-binding, not user auth). See spec.
const UNAUTH_PATHS = new Set(['/auth/login', '/auth/setup', '/auth/setup-required']); const UNAUTH_PATHS = new Set(['/auth/login', '/auth/setup', '/auth/setup-required']);
// Service-auth carve-outs: node-agent uses migration 019's bound-hostname
// api_token mechanism, not user auth. Today only /cluster/heartbeat is
// reached without a user session — operator/UI endpoints in cluster.js
// (containers restart, DELETE /:id, blackmagic device queries) ARE expected
// to require auth. If node-agent grows another endpoint, add it here.
// TODO: long-term, issue node-agent a real bound api_token and drop this carve-out.
const SERVICE_PATHS = new Set(['/cluster/heartbeat']);
app.use('/api/v1', (req, res, next) => { app.use('/api/v1', (req, res, next) => {
if (UNAUTH_PATHS.has(req.path)) return next(); if (UNAUTH_PATHS.has(req.path)) return next();
if (req.path.startsWith('/cluster')) return next(); // node-agent service auth, not user auth if (SERVICE_PATHS.has(req.path)) return next();
return requireAuth(req, res, next); return requireAuth(req, res, next);
}); });