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

@@ -2,6 +2,9 @@ import React, { createContext, useContext, useState, useEffect, useCallback } fr
const API_BASE = process.env.REACT_APP_API_BASE || 'http://localhost:3001/api';
// Known BU teams — must match backend helpers/teams.js
const KNOWN_TEAMS = ['STEAM', 'ACCESS-ENG', 'ACCESS-OPS', 'INTELDEV'];
const AuthContext = createContext(null);
export function AuthProvider({ children }) {
@@ -9,6 +12,11 @@ export function AuthProvider({ children }) {
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
// Admin scope toggle — persisted in localStorage
const [adminScope, setAdminScope] = useState(
() => localStorage.getItem('admin_bu_scope') || 'my-teams'
);
// Check if user is authenticated on mount
const checkAuth = useCallback(async () => {
try {
@@ -93,6 +101,46 @@ export function AuthProvider({ children }) {
// Check if user is admin
const isAdmin = () => isInGroup('Admin');
// -----------------------------------------------------------------------
// Multi-BU tenancy helpers
// -----------------------------------------------------------------------
// Whether the user has any BU teams assigned
const hasTeams = () => (user?.teams?.length ?? 0) > 0;
// Whether the user is a member of a specific team (or is Admin in "All BUs" mode)
const isTeamMember = (team) => {
if (!user) return false;
if (isInGroup('Admin') && adminScope === 'all') return true;
return (user.teams || []).includes(team);
};
// Toggle admin scope between 'my-teams' and 'all'
const toggleAdminScope = () => {
setAdminScope(prev => {
const next = prev === 'my-teams' ? 'all' : 'my-teams';
localStorage.setItem('admin_bu_scope', next);
return next;
});
};
// Returns the comma-joined teams string for API query params.
// Empty string means "no filter" (show all).
const getActiveTeamsParam = () => {
if (!user) return '';
if (isInGroup('Admin') && adminScope === 'all') return '';
const teams = user.teams || [];
return teams.join(',');
};
// Returns the list of teams available for UI selectors (compliance team picker, etc.)
// Admin in "All BUs" mode sees all known teams; otherwise scoped to user's teams.
const getAvailableTeams = () => {
if (!user) return [];
if (isInGroup('Admin') && adminScope === 'all') return KNOWN_TEAMS;
return user.teams || [];
};
const value = {
user,
loading,
@@ -105,7 +153,15 @@ export function AuthProvider({ children }) {
canDelete,
canExport,
isAdmin,
isAuthenticated: !!user
isAuthenticated: !!user,
// Multi-BU tenancy
hasTeams,
isTeamMember,
adminScope,
toggleAdminScope,
getActiveTeamsParam,
getAvailableTeams,
KNOWN_TEAMS,
};
return (