Extend team enforcement to Atlas and Archive routes, update schema reference
- Atlas: add requireTeam() at router level; replace client ?teams= param parsing with req.teamScope in /metrics, /status, and /sync endpoints - Archive: add requireTeam() at router level; replace client ?teams= param parsing with req.teamScope in GET / and GET /stats endpoints - db-schema.sql: add impersonate_user_id column to sessions table reference The frontend still sends ?teams= as a query param to these endpoints (harmless no-op since backend ignores it). Frontend cleanup deferred to avoid churn in the 7000-line ReportingPage component.
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
|
||||
const express = require('express');
|
||||
const pool = require('../db');
|
||||
const { requireAuth, requireGroup } = require('../middleware/auth');
|
||||
const { requireAuth, requireGroup, requireTeam } = require('../middleware/auth');
|
||||
const logAudit = require('../helpers/auditLog');
|
||||
const { isConfigured, atlasGet, atlasPut, atlasPatch, atlasPost } = require('../helpers/atlasApi');
|
||||
|
||||
@@ -70,49 +70,44 @@ function aggregateAtlasMetrics(rows) {
|
||||
function createAtlasRouter() {
|
||||
const router = express.Router();
|
||||
|
||||
// All atlas routes require authentication and team scoping
|
||||
router.use(requireAuth());
|
||||
router.use(requireTeam());
|
||||
|
||||
/**
|
||||
* GET /metrics
|
||||
*
|
||||
* Returns aggregated Atlas action plan metrics from the local cache.
|
||||
* Accepts optional `teams` query parameter to scope metrics to hosts
|
||||
* belonging to specific BUs (via JOIN on ivanti_findings).
|
||||
* Team scoping enforced by requireTeam() — scopes to hosts belonging to user's BUs.
|
||||
*
|
||||
* @query {string} [teams] - Comma-separated team names (e.g. 'STEAM,ACCESS-ENG')
|
||||
* @returns {Object} 200 - { totalHosts, hostsWithPlans, hostsWithoutPlans, plansByType, plansByStatus, totalPlans }
|
||||
* @returns {Object} 503 - { error } when Atlas API is not configured
|
||||
* @returns {Object} 500 - { error } on database failure
|
||||
*/
|
||||
router.get('/metrics', requireAuth(), async (req, res) => {
|
||||
router.get('/metrics', async (req, res) => {
|
||||
if (!isConfigured) {
|
||||
return res.status(503).json({ error: 'Atlas API is not configured. Check ATLAS_API_URL, ATLAS_API_USER, and ATLAS_API_PASS environment variables.' });
|
||||
}
|
||||
|
||||
try {
|
||||
const teamsParam = req.query.teams;
|
||||
let rows;
|
||||
|
||||
if (teamsParam) {
|
||||
const teams = teamsParam.split(',').map(t => t.trim()).filter(Boolean);
|
||||
if (teams.length > 0) {
|
||||
const patterns = teams.map(t => `%${t}%`);
|
||||
const result = await pool.query(
|
||||
`SELECT a.has_action_plan, a.plans_json
|
||||
FROM atlas_action_plans_cache a
|
||||
INNER JOIN (
|
||||
SELECT DISTINCT host_id FROM ivanti_findings
|
||||
WHERE bu_ownership ILIKE ANY($1::text[])
|
||||
) f ON a.host_id = f.host_id
|
||||
WHERE a.atlas_known = true`,
|
||||
[patterns]
|
||||
);
|
||||
rows = result.rows;
|
||||
} else {
|
||||
const result = await pool.query(
|
||||
`SELECT has_action_plan, plans_json FROM atlas_action_plans_cache WHERE atlas_known = true`
|
||||
);
|
||||
rows = result.rows;
|
||||
}
|
||||
if (req.teamScope) {
|
||||
// Non-admin: scope to user's team findings
|
||||
const patterns = req.teamScope.ivanti.map(t => `%${t}%`);
|
||||
const result = await pool.query(
|
||||
`SELECT a.has_action_plan, a.plans_json
|
||||
FROM atlas_action_plans_cache a
|
||||
INNER JOIN (
|
||||
SELECT DISTINCT host_id FROM ivanti_findings
|
||||
WHERE bu_ownership ILIKE ANY($1::text[])
|
||||
) f ON a.host_id = f.host_id
|
||||
WHERE a.atlas_known = true`,
|
||||
[patterns]
|
||||
);
|
||||
rows = result.rows;
|
||||
} else {
|
||||
// Admin bypass — all cached plans
|
||||
const result = await pool.query(
|
||||
`SELECT has_action_plan, plans_json FROM atlas_action_plans_cache WHERE atlas_known = true`
|
||||
);
|
||||
@@ -131,44 +126,35 @@ function createAtlasRouter() {
|
||||
* GET /status
|
||||
*
|
||||
* Returns atlas_action_plans_cache contents for status display.
|
||||
* Accepts optional `teams` query parameter to scope results to hosts
|
||||
* belonging to specific BUs (via JOIN on ivanti_findings).
|
||||
* Team scoping enforced by requireTeam() — scopes to hosts belonging to user's BUs.
|
||||
*
|
||||
* @query {string} [teams] - Comma-separated team names (e.g. 'STEAM,ACCESS-ENG')
|
||||
* @returns {Array} 200 - Array of { host_id, has_action_plan, plan_count, plans_json, atlas_known, synced_at }
|
||||
* @returns {Object} 503 - { error } when Atlas API is not configured
|
||||
* @returns {Object} 500 - { error } on database failure
|
||||
*/
|
||||
router.get('/status', requireAuth(), async (req, res) => {
|
||||
router.get('/status', async (req, res) => {
|
||||
if (!isConfigured) {
|
||||
return res.status(503).json({ error: 'Atlas API is not configured. Check ATLAS_API_URL, ATLAS_API_USER, and ATLAS_API_PASS environment variables.' });
|
||||
}
|
||||
|
||||
try {
|
||||
const teamsParam = req.query.teams;
|
||||
let rows;
|
||||
|
||||
if (teamsParam) {
|
||||
const teams = teamsParam.split(',').map(t => t.trim()).filter(Boolean);
|
||||
if (teams.length > 0) {
|
||||
const patterns = teams.map(t => `%${t}%`);
|
||||
const result = await pool.query(
|
||||
`SELECT a.host_id, a.has_action_plan, a.plan_count, a.plans_json, a.atlas_known, a.synced_at
|
||||
FROM atlas_action_plans_cache a
|
||||
INNER JOIN (
|
||||
SELECT DISTINCT host_id FROM ivanti_findings
|
||||
WHERE bu_ownership ILIKE ANY($1::text[])
|
||||
) f ON a.host_id = f.host_id`,
|
||||
[patterns]
|
||||
);
|
||||
rows = result.rows;
|
||||
} else {
|
||||
const result = await pool.query(
|
||||
`SELECT host_id, has_action_plan, plan_count, plans_json, atlas_known, synced_at FROM atlas_action_plans_cache`
|
||||
);
|
||||
rows = result.rows;
|
||||
}
|
||||
if (req.teamScope) {
|
||||
// Non-admin: scope to user's team findings
|
||||
const patterns = req.teamScope.ivanti.map(t => `%${t}%`);
|
||||
const result = await pool.query(
|
||||
`SELECT a.host_id, a.has_action_plan, a.plan_count, a.plans_json, a.atlas_known, a.synced_at
|
||||
FROM atlas_action_plans_cache a
|
||||
INNER JOIN (
|
||||
SELECT DISTINCT host_id FROM ivanti_findings
|
||||
WHERE bu_ownership ILIKE ANY($1::text[])
|
||||
) f ON a.host_id = f.host_id`,
|
||||
[patterns]
|
||||
);
|
||||
rows = result.rows;
|
||||
} else {
|
||||
// Admin bypass — all cached entries
|
||||
const result = await pool.query(
|
||||
`SELECT host_id, has_action_plan, plan_count, plans_json, atlas_known, synced_at FROM atlas_action_plans_cache`
|
||||
);
|
||||
@@ -187,64 +173,40 @@ function createAtlasRouter() {
|
||||
*
|
||||
* Syncs action plan data from Atlas for all hosts found in ivanti_findings.
|
||||
* Fetches plans per host in batches of 5 and upserts into the local cache.
|
||||
* Scopes to the provided teams or falls back to IVANTI_MANAGED_BUS.
|
||||
* Team scoping enforced by requireTeam() — syncs only hosts in user's BUs.
|
||||
* Falls back to IVANTI_MANAGED_BUS for admin when no team scope is set.
|
||||
* Requires Admin or Standard_User group.
|
||||
*
|
||||
* @query {string} [teams] - Comma-separated team names to scope sync (e.g. 'STEAM,ACCESS-ENG')
|
||||
* @param {Object} [req.body]
|
||||
* @param {string} [req.body.teams] - Comma-separated team names (alternative to query param)
|
||||
* @returns {Object} 200 - { synced, withPlans, failed }
|
||||
* @returns {Object} 503 - { error } when Atlas API is not configured
|
||||
* @returns {Object} 500 - { error } on unexpected failure
|
||||
*/
|
||||
router.post('/sync', requireAuth(), requireGroup('Admin', 'Standard_User'), async (req, res) => {
|
||||
router.post('/sync', requireGroup('Admin', 'Standard_User'), async (req, res) => {
|
||||
if (!isConfigured) {
|
||||
return res.status(503).json({ error: 'Atlas API is not configured. Check ATLAS_API_URL, ATLAS_API_USER, and ATLAS_API_PASS environment variables.' });
|
||||
}
|
||||
|
||||
try {
|
||||
// Scope sync to the user's active teams if provided, otherwise sync only
|
||||
// findings from managed BUs (IVANTI_MANAGED_BUS) to avoid polluting cache
|
||||
// with "no plan" entries for BUs not covered by Atlas.
|
||||
const teamsParam = req.query.teams || req.body.teams || '';
|
||||
// Use team scope from middleware, fall back to managed BUs for admin
|
||||
const managedBUs = (process.env.IVANTI_MANAGED_BUS || 'NTS-AEO-ACCESS-ENG,NTS-AEO-STEAM')
|
||||
.split(',').map(b => b.trim()).filter(Boolean);
|
||||
|
||||
let findingsRows;
|
||||
if (teamsParam) {
|
||||
const teams = teamsParam.split(',').map(t => t.trim()).filter(Boolean);
|
||||
if (teams.length > 0) {
|
||||
const patterns = teams.map(t => `%${t}%`);
|
||||
const result = await pool.query(
|
||||
`SELECT DISTINCT host_id FROM ivanti_findings
|
||||
WHERE host_id IS NOT NULL AND host_id > 0
|
||||
AND bu_ownership ILIKE ANY($1::text[])`,
|
||||
[patterns]
|
||||
);
|
||||
findingsRows = result.rows;
|
||||
} else {
|
||||
// No valid teams — fall back to managed BUs
|
||||
const patterns = managedBUs.map(b => `%${b}%`);
|
||||
const result = await pool.query(
|
||||
`SELECT DISTINCT host_id FROM ivanti_findings
|
||||
WHERE host_id IS NOT NULL AND host_id > 0
|
||||
AND bu_ownership ILIKE ANY($1::text[])`,
|
||||
[patterns]
|
||||
);
|
||||
findingsRows = result.rows;
|
||||
}
|
||||
let patterns;
|
||||
if (req.teamScope) {
|
||||
patterns = req.teamScope.ivanti.map(t => `%${t}%`);
|
||||
} else {
|
||||
// No teams specified — default to managed BUs only
|
||||
const patterns = managedBUs.map(b => `%${b}%`);
|
||||
const result = await pool.query(
|
||||
`SELECT DISTINCT host_id FROM ivanti_findings
|
||||
WHERE host_id IS NOT NULL AND host_id > 0
|
||||
AND bu_ownership ILIKE ANY($1::text[])`,
|
||||
[patterns]
|
||||
);
|
||||
findingsRows = result.rows;
|
||||
// Admin with no specific scope — sync managed BUs
|
||||
patterns = managedBUs.map(b => `%${b}%`);
|
||||
}
|
||||
|
||||
const result = await pool.query(
|
||||
`SELECT DISTINCT host_id FROM ivanti_findings
|
||||
WHERE host_id IS NOT NULL AND host_id > 0
|
||||
AND bu_ownership ILIKE ANY($1::text[])`,
|
||||
[patterns]
|
||||
);
|
||||
const findingsRows = result.rows;
|
||||
|
||||
const hostIds = findingsRows.map(r => r.host_id);
|
||||
|
||||
if (hostIds.length === 0) {
|
||||
|
||||
Reference in New Issue
Block a user