refactor(backend): replica guard helper for security routes (#797)

* refactor(backend): extract replica guard helper for security routes

Adds blockIfReplica(res, resource) in middleware/fleetSyncGuards.ts and
replaces six inline FleetSyncService.getRole() === 'replica' checks
across the security policies and CVE suppressions endpoints.

Error responses now use a uniform shape:
  403 { error: 'Cannot modify <resource> on a replica instance.
    Connect to the primary.', code: 'REPLICA_READ_ONLY' }

The new code field gives callers a stable discriminator without
matching prose.

Closes #750

* test(suppressions): match stable REPLICA_READ_ONLY code instead of prose

The replica guard helper exposes a stable code field for callers to
discriminate without grepping the human-readable error string. Switch
the replica-rejection assertion to use that code so the test no longer
breaks when the unified error template wording is tuned.
This commit is contained in:
Anso
2026-04-27 00:29:38 -04:00
committed by GitHub
parent 82894164e2
commit add3abaece
3 changed files with 31 additions and 25 deletions
+7 -24
View File
@@ -12,6 +12,7 @@ import { applySuppressions } from '../utils/suppression-filter';
import { generateSarif } from '../services/SarifExporter';
import { getErrorMessage } from '../utils/errors';
import { isDebugEnabled } from '../utils/debug';
import { blockIfReplica } from '../middleware/fleetSyncGuards';
import { FINDING_SEVERITIES, POLICY_SEVERITIES } from '../utils/severity';
const CVE_ID_RE = /^(CVE-\d{4}-\d{4,}|GHSA-[\w-]{14,})$/;
@@ -418,10 +419,7 @@ securityRouter.get('/policies', authMiddleware, (req: Request, res: Response): v
securityRouter.post('/policies', authMiddleware, (req: Request, res: Response): void => {
if (!requireAdmin(req, res)) return;
if (!requirePaid(req, res)) return;
if (FleetSyncService.getRole() === 'replica') {
res.status(403).json({ error: 'Security policies are managed from the control node.' });
return;
}
if (blockIfReplica(res, 'security policies')) return;
const { name, node_id, stack_pattern, max_severity, block_on_deploy, enabled } = req.body ?? {};
if (!name || typeof name !== 'string' || !name.trim()) {
res.status(400).json({ error: 'Policy name is required' }); return;
@@ -452,10 +450,7 @@ securityRouter.post('/policies', authMiddleware, (req: Request, res: Response):
securityRouter.put('/policies/:id', authMiddleware, (req: Request, res: Response): void => {
if (!requireAdmin(req, res)) return;
if (!requirePaid(req, res)) return;
if (FleetSyncService.getRole() === 'replica') {
res.status(403).json({ error: 'Security policies are managed from the control node.' });
return;
}
if (blockIfReplica(res, 'security policies')) return;
const id = Number(req.params.id);
if (!Number.isFinite(id)) {
res.status(400).json({ error: 'Invalid policy id' }); return;
@@ -488,10 +483,7 @@ securityRouter.put('/policies/:id', authMiddleware, (req: Request, res: Response
securityRouter.delete('/policies/:id', authMiddleware, (req: Request, res: Response): void => {
if (!requireAdmin(req, res)) return;
if (!requirePaid(req, res)) return;
if (FleetSyncService.getRole() === 'replica') {
res.status(403).json({ error: 'Security policies are managed from the control node.' });
return;
}
if (blockIfReplica(res, 'security policies')) return;
const id = Number(req.params.id);
if (!Number.isFinite(id)) {
res.status(400).json({ error: 'Invalid policy id' }); return;
@@ -514,10 +506,7 @@ securityRouter.get('/suppressions', authMiddleware, (req: Request, res: Response
securityRouter.post('/suppressions', authMiddleware, (req: Request, res: Response): void => {
if (!requireAdmin(req, res)) return;
if (!requirePaid(req, res)) return;
if (FleetSyncService.getRole() === 'replica') {
res.status(403).json({ error: 'CVE suppressions are managed from the control node.' });
return;
}
if (blockIfReplica(res, 'CVE suppressions')) return;
const body = req.body ?? {};
const cveId = typeof body.cve_id === 'string' ? body.cve_id.trim() : '';
if (!CVE_ID_RE.test(cveId)) {
@@ -570,10 +559,7 @@ securityRouter.post('/suppressions', authMiddleware, (req: Request, res: Respons
securityRouter.put('/suppressions/:id', authMiddleware, (req: Request, res: Response): void => {
if (!requireAdmin(req, res)) return;
if (!requirePaid(req, res)) return;
if (FleetSyncService.getRole() === 'replica') {
res.status(403).json({ error: 'CVE suppressions are managed from the control node.' });
return;
}
if (blockIfReplica(res, 'CVE suppressions')) return;
const id = Number(req.params.id);
if (!Number.isFinite(id)) {
res.status(400).json({ error: 'Invalid suppression id' }); return;
@@ -611,10 +597,7 @@ securityRouter.put('/suppressions/:id', authMiddleware, (req: Request, res: Resp
securityRouter.delete('/suppressions/:id', authMiddleware, (req: Request, res: Response): void => {
if (!requireAdmin(req, res)) return;
if (!requirePaid(req, res)) return;
if (FleetSyncService.getRole() === 'replica') {
res.status(403).json({ error: 'CVE suppressions are managed from the control node.' });
return;
}
if (blockIfReplica(res, 'CVE suppressions')) return;
const id = Number(req.params.id);
if (!Number.isFinite(id)) {
res.status(400).json({ error: 'Invalid suppression id' }); return;