feat: add multi-BU tenancy with per-user team scoping (Option B)

- Add bu_teams column to users table (migration + fresh schema)
- Create shared KNOWN_TEAMS constant and validateTeams helper
- Expose user teams in auth middleware, login, and /me responses
- Add bu_teams CRUD to user management routes with audit logging
- Make Ivanti FINDINGS_FILTERS configurable via IVANTI_BU_FILTER env var
- Add query-time team filtering to GET /findings and /findings/counts
- Update AuthContext with teams helpers and admin scope toggle
- Create AdminScopeToggle component (My Teams / All BUs)
- Scope ReportingPage findings fetch by user teams
- Scope CompliancePage team selector by user teams
- Scope ExportsPage findings exports by user teams
- Add BU teams multi-select to UserManagement create/edit forms
- Display team badges in user list table
This commit is contained in:
Jordan Ramos
2026-05-05 11:04:53 -06:00
parent af951fdc12
commit 2656df94d3
24 changed files with 999 additions and 127 deletions

View File

@@ -510,6 +510,36 @@ export default function AtlasSlideOutPanel({ hostId, hostName, findingId, qualys
}
const data = await res.json();
const remotePlans = parseAtlasPlans(data);
// If Atlas returns no plans, check local cache for optimistic bulk-create stubs
if (remotePlans.length === 0) {
try {
const cacheRes = await fetch(`${API_BASE}/atlas/status`, { credentials: 'include' });
if (cacheRes.ok) {
const cacheData = await cacheRes.json();
const hostCache = cacheData.find(r => r.host_id === hostId);
if (hostCache && hostCache.has_action_plan === 1 && hostCache.plans_json) {
let cachedPlans = [];
try { cachedPlans = typeof hostCache.plans_json === 'string' ? JSON.parse(hostCache.plans_json) : hostCache.plans_json; } catch (_) {}
const stubs = cachedPlans
.filter(p => p.source === 'bulk-create')
.map((p, i) => ({
action_plan_id: 'pending-' + hostId + '-' + i,
plan_type: p.plan_type || 'unknown',
commit_date: p.commit_date || '',
status: 'pending',
_localPending: true,
created_at: p.created_at || '',
}));
if (stubs.length > 0) {
setPlans(stubs);
return;
}
}
}
} catch (_) { /* ignore cache fallback errors */ }
}
// Merge: keep local pending plans that aren't yet confirmed by Atlas
setPlans(prev => {
const localPending = prev.filter(p => p._localPending);