# Implementation Plan: Multi-BU Tenancy (Option B) ## Overview Add per-user BU team assignment with query-time filtering on the Reporting and Compliance pages. Uses a single broadened Ivanti sync and filters cached findings based on the logged-in user's assigned teams. The CVE home page remains shared/global. ## Tasks - [x] 1. Database migration and shared teams constant - [x] 1.1 Create `backend/helpers/teams.js` shared constant - Export `KNOWN_TEAMS = ['STEAM', 'ACCESS-ENG', 'ACCESS-OPS', 'INTELDEV']` - Export `validateTeams(teamsString)` helper that parses and validates a comma-separated string - Returns `{ valid: boolean, teams: string[], invalid: string[] }` - _Requirements: 1.3, 1.5_ - [x] 1.2 Create `backend/migrations/add_user_bu_teams.js` - Add `bu_teams` column (TEXT, NOT NULL, DEFAULT '') to users table - Idempotent — check if column exists before adding - Follow existing migration pattern (open db, serialize, log progress, close) - _Requirements: 1.1, 1.2_ - [x] 1.3 Update `backend/setup.js` to include bu_teams in fresh schema - Add `bu_teams TEXT NOT NULL DEFAULT ''` to the users CREATE TABLE statement - _Requirements: 1.1, 1.2_ - [x] 2. Backend auth changes — expose teams in session - [x] 2.1 Update `backend/middleware/auth.js` requireAuth - Ensure the session JOIN query SELECTs `bu_teams` from users table - Parse `bu_teams` into array and attach as `req.user.teams` - Empty string becomes empty array `[]` - _Requirements: 2.1, 2.4_ - [x] 2.2 Update `backend/routes/auth.js` login response - Include `teams` array in the login success response object - _Requirements: 2.3_ - [x] 2.3 Update `backend/routes/auth.js` GET /me endpoint - Include `teams` array in the /me response - _Requirements: 2.2_ - [x] 3. User management — CRUD for bu_teams - [x] 3.1 Update `backend/routes/users.js` POST / (create user) - Accept optional `bu_teams` field in request body - Validate using `validateTeams()` helper — return 400 if invalid teams present - Store validated comma-separated string in DB - _Requirements: 1.4, 1.5, 6.1_ - [x] 3.2 Update `backend/routes/users.js` PATCH /:id (update user) - Accept optional `bu_teams` field in request body - Validate using `validateTeams()` helper — return 400 if invalid teams present - Log previous and new bu_teams values in audit trail - _Requirements: 1.4, 1.5, 6.2, 6.3_ - [x] 3.3 Update `backend/routes/users.js` GET endpoints - Include `bu_teams` (as raw string) and `teams` (as parsed array) in user response objects - _Requirements: 6.4_ - [x] 4. Checkpoint: Verify migration and auth - Run migration, create a test user with bu_teams, login, verify /me returns teams array. - [x] 5. Broaden Ivanti sync filter - [x] 5.1 Make FINDINGS_FILTERS BU value configurable in `backend/routes/ivantiFindings.js` - Read `process.env.IVANTI_BU_FILTER` — default to `'NTS-AEO-ACCESS-ENG,NTS-AEO-STEAM'` - Replace hardcoded value in FINDINGS_FILTERS array - Apply same change to CLOSED_COUNT_FILTERS - _Requirements: 3.1, 3.2, 3.3_ - [x] 5.2 Update `backend/.env.example` with IVANTI_BU_FILTER documentation - Add commented variable with explanation of how broadening works - _Requirements: 3.4_ - [x] 6. Add query-time team filtering to findings endpoints - [x] 6.1 Update GET /findings endpoint in `backend/routes/ivantiFindings.js` - Accept optional `teams` query parameter (comma-separated) - Filter findings array by buOwnership matching (case-insensitive includes) - Return filtered count in response - If teams param is empty/missing, return all findings (backward-compatible) - _Requirements: 4.1, 4.2, 4.3_ - [x] 6.2 Update GET /findings/counts endpoint - Accept optional `teams` query parameter - Filter open count by matching buOwnership against provided teams - Closed count: if teams provided, note in response that closed count is approximate (full cache doesn't store per-finding closed data) - _Requirements: 8.1, 8.2_ - [x] 6.3 Update GET /findings/counts/history endpoint (if team-scoped history is needed) - Consider whether historical counts should be team-scoped or remain global - For MVP: keep global (history is aggregate), document limitation - _Requirements: 8.3_ - [x] 7. Checkpoint: Verify backend filtering - Test GET /findings with and without teams param. Verify filtering works correctly. - [x] 8. Frontend AuthContext — expose teams and admin scope toggle - [x] 8.1 Update `frontend/src/contexts/AuthContext.js` - Store `user.teams` from /me response - Add `hasTeams()` helper — returns true if teams array is non-empty - Add `isTeamMember(team)` helper — returns true if team is in user's teams or user is Admin - Add `adminScope` state backed by localStorage key `admin_bu_scope` (default: `'my-teams'`) - Add `toggleAdminScope()` function to flip between `'my-teams'` and `'all'` - Add `getActiveTeamsParam()` helper — returns comma-joined teams for API calls; returns empty string when Admin is in "All BUs" mode - Add `getAvailableTeams(knownTeams)` helper — returns user's teams, or all knownTeams if Admin in "All BUs" mode - _Requirements: 2.2, 4.4, 4.6, 4.7, 4.9, 10.1, 10.2, 10.5, 10.6_ - [x] 9. Frontend Admin Scope Toggle component - [x] 9.1 Create `frontend/src/components/AdminScopeToggle.js` - Render a two-state toggle: "My Teams" | "All BUs" - Only visible when `isAdmin()` is true - Calls `toggleAdminScope()` from AuthContext on click - Styled to fit in the top nav/header area - _Requirements: 10.1, 10.2, 10.7_ - [x] 9.2 Mount AdminScopeToggle in the app header/nav - Place in NavDrawer or top bar so it's accessible from any page - _Requirements: 10.7_ - [x] 10. Frontend Reporting Page — scope by teams - [x] 10.1 Update findings fetch in `ReportingPage.js` - Use `getActiveTeamsParam()` from AuthContext to build the teams query param - If user has no teams and is not Admin, show "No BU teams assigned" info panel instead of table - _Requirements: 4.4, 4.5_ - [x] 10.2 Update counts fetch in `ReportingPage.js` - Append teams param to counts endpoint using same `getActiveTeamsParam()` - _Requirements: 8.2_ - [x] 10.3 Re-fetch findings when admin scope toggle changes - Listen to `adminScope` value from AuthContext; trigger re-fetch on change - _Requirements: 4.8_ - [x] 11. Frontend Compliance Page — scope team selector - [x] 11.1 Update `CompliancePage.js` TEAMS constant - Replace hardcoded `['STEAM', 'ACCESS-ENG']` with `getAvailableTeams(KNOWN_TEAMS)` from AuthContext - Non-admin sees only their assigned teams - Admin in "My Teams" mode sees only their assigned teams - Admin in "All BUs" mode sees all KNOWN_TEAMS - _Requirements: 5.1, 5.4, 5.5_ - [x] 11.2 Handle single-team and no-team states - If user has one team: default to it, optionally hide selector - If user has no teams and is not Admin: show "No teams assigned" message - _Requirements: 5.2, 5.3_ - [ ] 11.3 Update `ComplianceChartsPanel.js` team colors - Ensure chart only renders data for user's available teams - _Requirements: 5.1_ - [x] 11.4 Re-render team selector when admin scope toggle changes - Listen to `adminScope` from AuthContext; update available teams list on change - _Requirements: 5.5, 5.6_ - [x] 12. Frontend Exports Page — scope by teams - [x] 12.1 Update `ExportsPage.js` findings fetch - Use `getActiveTeamsParam()` to pass teams filter - _Requirements: 9.1_ - [x] 12.2 Add BU scope indicator to export filenames - Append team names to exported file names (e.g. `findings-STEAM-2026-05-05.xlsx`) - When in "All BUs" mode, use `findings-ALL-2026-05-05.xlsx` - _Requirements: 9.3_ - [ ] 13. Frontend User Management — BU team assignment UI - [ ] 13.1 Add team multi-select to create user form in `UserManagement.js` - Checkbox group with all KNOWN_TEAMS options - Sends comma-separated string as `bu_teams` field - _Requirements: 6.1_ - [ ] 13.2 Add team multi-select to edit user form - Pre-populate with user's current teams - _Requirements: 6.2_ - [ ] 13.3 Display teams in user list table - Show team badges for each user - Show warning indicator for users with no teams assigned - _Requirements: 6.4, 6.5_ - [ ] 14. Verify CVE page is unaffected - [ ] 14.1 Confirm no team filtering on CVE routes or frontend - Verify CVE page does not reference user.teams for data fetching - _Requirements: 7.1, 7.2, 7.3_ - [ ] 15. Final checkpoint: End-to-end verification - Create two test users with different bu_teams assignments. Log in as each and verify: - Reporting page shows only their BU's findings - Compliance page shows only their teams in selector - CVE page shows same data for both - Exports are scoped correctly - Admin scope toggle switches between "My Teams" and "All BUs" correctly - Admin in "My Teams" mode sees same scoped view as a regular user with same teams - Toggle state persists across page refresh ## Notes - The `bu_teams` field is a UX scoping mechanism, not a hard security boundary. Admin and direct API callers can always access all data. - The buOwnership matching uses case-insensitive `includes()` to handle Ivanti's full BU names (e.g. "NTS-AEO-STEAM" matches user team "STEAM"). - Historical counts (IvantiCountsChart) remain global for MVP — per-BU historical tracking would require schema changes to the counts history table. - The compliance backend endpoints are unchanged — only the frontend team selector is scoped. - After running the migration, an admin must assign bu_teams to existing users. Until then, non-Admin users with empty teams will see the "no teams assigned" message on Reporting/Compliance. - The Admin scope toggle defaults to "My Teams" so that your daily workflow (STEAM + ACCESS-ENG) is the default view. Switch to "All BUs" when you need the full picture across all business units. - The scope toggle state is shared across Reporting, Compliance, and Exports — one toggle controls all three pages consistently. - Admin users with empty bu_teams: "My Teams" mode behaves identically to "All BUs" (no filtering in either case).