Files
cve-dashboard/.kiro/specs/multi-bu-tenancy/tasks.md

202 lines
10 KiB
Markdown

# 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).