Add backend team enforcement via requireTeam() middleware

Introduce server-side team-scoped data access enforcement:

- Add TEAM_TO_IVANTI/IVANTI_TO_TEAM mapping to helpers/teams.js
- Add requireTeam() middleware to middleware/auth.js
  - Admin bypass (req.teamScope = null)
  - 403 for users with no team assignment
  - Populates req.teamScope with short and ivanti name arrays
- Ivanti findings: replace client ?teams= param with req.teamScope filtering
  on GET /, /counts, /counts/history, /fp-workflow-counts, POST /sync
  - Override and note endpoints verify finding is in team scope
- Compliance: add requireTeam() router-level, validate ?team= param against scope
  on GET /items and GET /summary
- CARD: validate teamName param on GET /teams/:teamName/assets
- Todo queue: verify findings belong to user's teams on POST /batch
- Clarify IVANTI_BU_FILTER comment (sync-level vs query-time filtering)
- Update 14 test files to include requireTeam in auth middleware mocks
This commit is contained in:
Jordan Ramos
2026-06-24 11:36:25 -06:00
parent ab66d7d813
commit a003091b6a
20 changed files with 239 additions and 81 deletions

View File

@@ -7,7 +7,7 @@ const fs = require('fs');
const crypto = require('crypto');
const { spawn } = require('child_process');
const pool = require('../db');
const { requireAuth, requireGroup } = require('../middleware/auth');
const { requireAuth, requireGroup, requireTeam } = require('../middleware/auth');
const { loadConfig, compareSchemaToDrift, reconcileConfig } = require('../helpers/driftChecker');
const { isValidDateString, validateRemediationPlan, computeVCLStats, categorizeNonCompliant, rankHeavyHitters, computeForecastBurndown, matchByHostname, computeBulkDiff, mapColumnHeaders } = require('../helpers/vclHelpers');
const logAudit = require('../helpers/auditLog');
@@ -288,8 +288,9 @@ function computeWaterfall(uploads) {
function createComplianceRouter(upload) {
const router = express.Router();
// All compliance routes require authentication
// All compliance routes require authentication and team assignment
router.use(requireAuth());
router.use(requireTeam());
/**
* POST /preview
@@ -537,6 +538,16 @@ function createComplianceRouter(upload) {
const team = req.query.team;
if (team && !ALLOWED_TEAMS.has(team)) return res.status(400).json({ error: 'Invalid team' });
// Validate requested team is in user's scope
if (team && req.teamScope && !req.teamScope.short.includes(team)) {
return res.status(403).json({
error: 'Access denied. You do not have access to the requested team.',
code: 'TEAM_ACCESS_DENIED',
requested: team,
allowed: req.teamScope.short
});
}
try {
// Try AEO uploads first (vertical IS NULL), fall back to NTS_AEO multi-vertical upload
let { rows: latestRows } = await pool.query(
@@ -600,6 +611,16 @@ function createComplianceRouter(upload) {
if (!ALLOWED_TEAMS.has(team)) return res.status(400).json({ error: 'Invalid team' });
if (!['active', 'resolved'].includes(status)) return res.status(400).json({ error: 'Invalid status' });
// Validate requested team is in user's scope
if (req.teamScope && !req.teamScope.short.includes(team)) {
return res.status(403).json({
error: 'Access denied. You do not have access to the requested team.',
code: 'TEAM_ACCESS_DENIED',
requested: team,
allowed: req.teamScope.short
});
}
try {
// Include items from both AEO uploads (vertical IS NULL) and NTS_AEO multi-vertical uploads
// DISTINCT ON deduplicates cross-vertical (hostname, metric_id) pairs, keeping the representative row