From d9db054bfd757c785d11f2c29a18a27defd3f19c Mon Sep 17 00:00:00 2001 From: Ahmed Darrazi Date: Sun, 4 Jan 2026 14:53:48 +0100 Subject: [PATCH] spec: tenant portfolio & context switch (031) --- .../checklists/requirements.md | 14 +++ .../plan.md | 34 +++++++ .../spec.md | 89 +++++++++++++++++++ .../tasks.md | 34 +++++++ 4 files changed, 171 insertions(+) create mode 100644 specs/031-tenant-portfolio-context-switch/checklists/requirements.md create mode 100644 specs/031-tenant-portfolio-context-switch/plan.md create mode 100644 specs/031-tenant-portfolio-context-switch/spec.md create mode 100644 specs/031-tenant-portfolio-context-switch/tasks.md diff --git a/specs/031-tenant-portfolio-context-switch/checklists/requirements.md b/specs/031-tenant-portfolio-context-switch/checklists/requirements.md new file mode 100644 index 0000000..5162cf1 --- /dev/null +++ b/specs/031-tenant-portfolio-context-switch/checklists/requirements.md @@ -0,0 +1,14 @@ +# Requirements Checklist (031) + +**Created**: 2026-01-04 +**Feature**: [spec.md](../spec.md) + +- [ ] Tenant memberships/roles exist and are enforced. +- [ ] Current Tenant context is per-user and always visible. +- [ ] Portfolio shows only accessible tenants with environment + health/status. +- [ ] “Open tenant” changes context and redirects into tenant-scoped area. +- [ ] Tenant-scoped resources are filtered by context and deny unauthorized access. +- [ ] Bulk “Sync selected” dispatches per-tenant jobs and is role-gated. +- [ ] Restore flows show target tenant + environment and require tenant-aware confirmation. +- [ ] Pest tests cover authorization + context switching + bulk actions. + diff --git a/specs/031-tenant-portfolio-context-switch/plan.md b/specs/031-tenant-portfolio-context-switch/plan.md new file mode 100644 index 0000000..f32db6c --- /dev/null +++ b/specs/031-tenant-portfolio-context-switch/plan.md @@ -0,0 +1,34 @@ +# Plan: Tenant Portfolio & Context Switch (031) + +**Branch**: `feat/031-tenant-portfolio-context-switch` +**Date**: 2026-01-04 +**Input**: [spec.md](./spec.md) + +## Approach +1. Decide on the tenant context mechanism: + - Preferred: Filament tenancy (tenant in URL) + built-in tenant switcher. + - Fallback: session-based Current Tenant + visible banner (avoid global/DB state as “source of truth”). +2. Add data model pieces: + - tenant membership/role mapping + - tenant environment attribute + - optional user preferences (favorites + last used) +3. Implement a single TenantContext resolver (HTTP + console) and central authorization gate/policy: + - deny-by-default if no access + - keep `INTUNE_TENANT_ID` as console override for automation +4. Update tenant-scoped resources/services to use TenantContext instead of `Tenant::current()` and ensure base queries are tenant-scoped. +5. Extend `TenantResource` into a portfolio view: + - access-scoped query + - environment/health columns + - “Open” action + “Sync” action + - bulk “Sync selected” +6. Add restore guardrails: + - target tenant badge/header on restore pages + - type-to-confirm includes tenant/environment (e.g. `RESTORE PROD`) +7. Add targeted Pest tests for authorization, context switching, and bulk sync. +8. Run Pint + targeted tests; document rollout/migration notes. + +## Decisions / Notes +- Avoid a global `tenants.is_current` UI context (unsafe for MSP); prefer per-user context. +- Avoid storing Current Tenant in the `users` table as the source of truth (cross-tab risk); prefer route/session context, optionally persisting “last used” separately. +- Start with user-based tenant memberships; extend to organization/group principals later if needed. +- Prefer deriving portfolio stats via relationships (`withCount`, `withMax`) initially; add denormalized summary columns only if needed for performance. diff --git a/specs/031-tenant-portfolio-context-switch/spec.md b/specs/031-tenant-portfolio-context-switch/spec.md new file mode 100644 index 0000000..d90ade4 --- /dev/null +++ b/specs/031-tenant-portfolio-context-switch/spec.md @@ -0,0 +1,89 @@ +# Feature Specification: Tenant Portfolio & Context Switch (031) + +**Feature Branch**: `feat/031-tenant-portfolio-context-switch` +**Created**: 2026-01-04 +**Status**: Proposed +**Risk**: Medium +**Priority**: P1 + +## Context +Today TenantPilot behaves like a single-tenant app: +- The “current tenant” is global (`tenants.is_current` + `Tenant::current()`), not per user. +- Most tenant-scoped screens implicitly use `Tenant::current()`. + +This is limiting and potentially unsafe for: +- Customers running multiple tenants (PROD/DEV/STAGING). +- MSPs managing many customer tenants. + +We need a tenant-agnostic **Portfolio** view plus an explicit, always-visible **Current Tenant** context for all tenant-scoped areas (Policies, Backups, Restore Runs, etc.). + +## Design Considerations (Best Practice) +- Prefer an explicit tenant context (route parameter or session) over hidden global state. +- Avoid storing Current Tenant in the `users` table as the source of truth (cross-tab risk). If persistence is needed, store **“last used”** separately and treat it as a default for new sessions. +- Keep console/automation behavior stable: `INTUNE_TENANT_ID` can remain a console override, but tenant-scoped UI must not depend on it. + +## User Scenarios & Testing + +### User Story 1 — Portfolio overview (P1) +As a user with access to multiple tenants, I can see a portfolio overview with health/status and key counts. + +**Acceptance Scenarios** +1. Tenants list shows only tenants the user can access. +2. Portfolio shows environment badge (PROD/DEV/STAGING/OTHER) and connection/health indicators. +3. Portfolio columns can be filtered by environment and connection status. + +### User Story 2 — Safe tenant context switching (P1) +As a user, I can switch the Current Tenant via a topbar switcher or by clicking “Open” in the portfolio, and all tenant-scoped screens reflect that tenant. + +**Acceptance Scenarios** +1. Switching tenant updates the visible Current Tenant badge and redirects to a default tenant-scoped landing page (e.g. Policies). +2. Policies, Backups, Restore Runs, and Policy Versions are scoped to the selected tenant. +3. Restore flows always show the target tenant and environment prominently and require tenant-aware type-to-confirm. + +### User Story 3 — Multi-tenant bulk actions (P2) +As an operator, I can select multiple tenants in the portfolio and run safe bulk actions (initially Sync). + +**Acceptance Scenarios** +1. Bulk “Sync selected” dispatches a sync job per tenant (batch) and shows progress. +2. Readonly users cannot trigger bulk sync. + +### User Story 4 — Authorization hardening (P1) +As a user, I cannot access tenants or tenant-scoped data I am not authorized for. + +**Acceptance Scenarios** +1. Attempting to open a tenant without access is denied (403) and does not change Current Tenant. +2. Direct URL access to tenant-scoped pages for an unauthorized tenant returns 403/404. + +## Requirements + +### Functional Requirements +- **FR-001**: Introduce a per-user Current Tenant context for all tenant-scoped screens. +- **FR-002**: Current Tenant context must be always visible in the UI (topbar) to reduce “wrong tenant” operations. +- **FR-003**: Add an “Open” action from the portfolio to set Current Tenant and redirect into the tenant-scoped area. +- **FR-004**: Portfolio view is tenant-agnostic and supports filtering, search, and safe bulk actions. +- **FR-005**: Tenant access is enforced centrally (single `canAccessTenant(...)` gate/policy used by UI + routes + services). +- **FR-006**: Restore remains single-tenant; restore actions must include explicit tenant/environment confirmations and never rely on hidden global context. +- **FR-007**: Bulk Sync is tenant-safe: per-tenant authorization, per-tenant job execution, and audit logs for each tenant sync trigger. + +### UX / UI Requirements +- **UX-001**: Topbar shows “Tenant: ” with an environment badge (PROD/DEV/STAGING/OTHER) and is accessible from all tenant-scoped pages. +- **UX-002**: Tenant switcher is searchable (typeahead); favorites (if enabled) appear at the top. +- **UX-003**: Portfolio table includes (at minimum): Name, Tenant ID (short/copy), Environment, Connection/App status, RBAC/Health indicator, Last Sync (time), Policies count; optional Restore runs (last 30d). +- **UX-004**: Portfolio “Open” action makes the tenant context explicit and navigates into the tenant-scoped area. +- **UX-005**: Restore screens show “Target Tenant” prominently (name + environment badge) and require tenant-aware type-to-confirm (e.g. `RESTORE PROD`). + +### Data Model Requirements +- **DM-001**: Introduce tenant access/membership mapping (user ↔ tenant) with a role (`owner|manager|operator|readonly`). +- **DM-002**: Add tenant environment classification (`prod|dev|staging|other`) as a first-class attribute (column or indexed JSONB). +- **DM-003 (Optional)**: Persist per-user tenant preferences (favorites + last used) without coupling it to cross-tab safety. +- **DM-004 (Optional)**: Support grouping tenants by customer (MSP use case) via a lightweight “customer label” or a dedicated Customer model (future). + +## Non-Goals +- No multi-tenant policy detail view in one screen. +- No multi-tenant restore; restore run always targets exactly one tenant. +- No cross-tenant diff/promotion (separate feature). + +## Success Criteria +- **SC-001**: A user can switch Current Tenant quickly and always understands which tenant they are operating on. +- **SC-002**: All tenant-scoped data is strictly filtered and authorization-safe. +- **SC-003**: Bulk Sync works across selected tenants with clear feedback and role gating. diff --git a/specs/031-tenant-portfolio-context-switch/tasks.md b/specs/031-tenant-portfolio-context-switch/tasks.md new file mode 100644 index 0000000..3a2579a --- /dev/null +++ b/specs/031-tenant-portfolio-context-switch/tasks.md @@ -0,0 +1,34 @@ +# Tasks: Tenant Portfolio & Context Switch (031) + +**Branch**: `feat/031-tenant-portfolio-context-switch` +**Date**: 2026-01-04 +**Input**: [spec.md](./spec.md), [plan.md](./plan.md) + +## Phase 1: Setup +- [x] T001 Create spec/plan/tasks and checklist. + +## Phase 2: Research & Design +- [ ] T002 Review Filament tenancy support and choose the context mechanism (route vs session). +- [ ] T003 Define tenant access roles and mapping (user memberships; future org/group principals). +- [ ] T004 Decide how to store `environment` (column vs JSONB) and whether MSP “customer grouping” is in scope. +- [ ] T005 Define context precedence rules (env override, route tenant, session/default tenant) and cross-tab safety expectations. + +## Phase 3: Tests (TDD) +- [ ] T006 Authorization: user cannot open unauthorized tenant (403). +- [ ] T007 Authorization: tenant-scoped resources deny cross-tenant access via URL (403/404). +- [ ] T008 Context switching: “Open tenant” sets context and tenant-scoped pages filter correctly. +- [ ] T009 Bulk sync: dispatches one job per selected tenant; readonly role cannot run it. +- [ ] T010 UI (optional browser tests): tenant switcher visible and environment badge shown. + +## Phase 4: Implementation +- [ ] T011 Add migrations for tenant memberships/roles and environment attribute (and optional preferences). +- [ ] T012 Implement `TenantContext` + authorization gate/policy (`canAccessTenant`). +- [ ] T013 Integrate tenant switcher into Filament topbar and make Current Tenant always visible. +- [ ] T014 Scope tenant resources (Policies/Backups/RestoreRuns/etc.) via TenantContext; replace direct `Tenant::current()` usage. +- [ ] T015 Update `TenantResource` into a portfolio view: access-scoped query, columns, filters, “Open”, “Sync”, bulk “Sync selected”. +- [ ] T016 Add restore guardrails (target tenant header + tenant-aware confirmations). + +## Phase 5: Verification +- [ ] T017 Run targeted tests. +- [ ] T018 Run Pint (`./vendor/bin/pint --dirty`). +