feat: move trivy auto-update, node labels, fleet topology, and single-scan SBOM to Community (#1336)

Rebalance several capabilities from the paid tier to the free Community tier:

- Managed Trivy auto-update toggle is admin-only, no longer tier-gated.
- Node labels (assign, view, manage) are available on every tier; cordon and
  FleetSync anchor reset stay paid.
- Fleet topology layout modes (Hub, Grouped, Free) are available on Community.
- Single-scan SBOM export (SPDX and CycloneDX) is admin-only on Community;
  SARIF export stays paid via a dedicated canExportSarif capability split out
  from the former shared SBOM flag.

Backend route guards and frontend affordances are updated together, with tier
and admin-role tests covering the Community-allowed and still-paid paths.
This commit is contained in:
Anso
2026-06-08 08:58:14 -04:00
committed by GitHub
parent 710647a44f
commit 54119be0c2
18 changed files with 214 additions and 74 deletions
+1 -6
View File
@@ -1,6 +1,6 @@
import { Router, type Request, type Response } from 'express';
import { authMiddleware } from '../middleware/auth';
import { requirePaid, requireAdmin, requireBody } from '../middleware/tierGates';
import { requireAdmin, requireBody } from '../middleware/tierGates';
import { DatabaseService } from '../services/DatabaseService';
import { NodeLabelService } from '../services/NodeLabelService';
import { parseIntParam } from '../utils/parseIntParam';
@@ -10,7 +10,6 @@ export const nodeLabelsRouter = Router();
nodeLabelsRouter.use(authMiddleware);
nodeLabelsRouter.get('/', (req: Request, res: Response): void => {
if (!requirePaid(req, res)) return;
try {
const map = NodeLabelService.getInstance().listAll();
res.json(map);
@@ -21,7 +20,6 @@ nodeLabelsRouter.get('/', (req: Request, res: Response): void => {
});
nodeLabelsRouter.get('/all', (req: Request, res: Response): void => {
if (!requirePaid(req, res)) return;
try {
const labels = NodeLabelService.getInstance().listDistinct();
res.json({ labels });
@@ -32,7 +30,6 @@ nodeLabelsRouter.get('/all', (req: Request, res: Response): void => {
});
nodeLabelsRouter.get('/:nodeId', (req: Request, res: Response): void => {
if (!requirePaid(req, res)) return;
const nodeId = parseIntParam(req, res, 'nodeId');
if (nodeId === null) return;
try {
@@ -50,7 +47,6 @@ nodeLabelsRouter.get('/:nodeId', (req: Request, res: Response): void => {
});
nodeLabelsRouter.post('/:nodeId', (req: Request, res: Response): void => {
if (!requirePaid(req, res)) return;
if (!requireAdmin(req, res)) return;
if (!requireBody(req, res)) return;
const nodeId = parseIntParam(req, res, 'nodeId');
@@ -75,7 +71,6 @@ nodeLabelsRouter.post('/:nodeId', (req: Request, res: Response): void => {
});
nodeLabelsRouter.delete('/:nodeId/:label', (req: Request, res: Response): void => {
if (!requirePaid(req, res)) return;
if (!requireAdmin(req, res)) return;
const nodeId = parseIntParam(req, res, 'nodeId');
if (nodeId === null) return;