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

10 KiB

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

  • 1. Database migration and shared teams constant

    • 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
    • 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
    • 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
  • 2. Backend auth changes — expose teams in session

    • 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
    • 2.2 Update backend/routes/auth.js login response

      • Include teams array in the login success response object
      • Requirements: 2.3
    • 2.3 Update backend/routes/auth.js GET /me endpoint

      • Include teams array in the /me response
      • Requirements: 2.2
  • 3. User management — CRUD for bu_teams

    • 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
    • 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
    • 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
  • 4. Checkpoint: Verify migration and auth

    • Run migration, create a test user with bu_teams, login, verify /me returns teams array.
  • 5. Broaden Ivanti sync filter

    • 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
    • 5.2 Update backend/.env.example with IVANTI_BU_FILTER documentation

      • Add commented variable with explanation of how broadening works
      • Requirements: 3.4
  • 6. Add query-time team filtering to findings endpoints

    • 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
    • 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
    • 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
  • 7. Checkpoint: Verify backend filtering

    • Test GET /findings with and without teams param. Verify filtering works correctly.
  • 8. Frontend AuthContext — expose teams and admin scope toggle

    • 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
  • 9. Frontend Admin Scope Toggle component

    • 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
    • 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
  • 10. Frontend Reporting Page — scope by teams

    • 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
    • 10.2 Update counts fetch in ReportingPage.js

      • Append teams param to counts endpoint using same getActiveTeamsParam()
      • Requirements: 8.2
    • 10.3 Re-fetch findings when admin scope toggle changes

      • Listen to adminScope value from AuthContext; trigger re-fetch on change
      • Requirements: 4.8
  • 11. Frontend Compliance Page — scope team selector

    • 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
    • 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
    • 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
  • 12. Frontend Exports Page — scope by teams

    • 12.1 Update ExportsPage.js findings fetch

      • Use getActiveTeamsParam() to pass teams filter
      • Requirements: 9.1
    • 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).