TenantAtlas/specs/068-workspaces-v2/plan.md
2026-02-01 12:19:57 +01:00

193 lines
9.7 KiB
Markdown

# Implementation Plan: Workspace Model, Memberships & Managed Tenants (v2)
**Branch**: `068-workspaces-v2` | **Date**: 2026-01-31 | **Spec**: [spec.md](spec.md)
**Input**: Feature specification from [spec.md](spec.md)
**Note**: This template is filled in by the `/speckit.plan` command. See `.specify/scripts/` for helper scripts.
## Summary
Introduce a Workspace hierarchy as the primary scope for the tenant-plane admin panel:
- Workspace-scoped routing (`/admin/w/{workspace}/...`) with deny-as-not-found (404 semantics) for non-members.
- Workspace memberships (owner/manager/operator/readonly) as the source of truth for access and capability derivation.
- Workspace switcher + choose-workspace/no-access entry flows with session + `users.last_workspace_id` persistence.
- Managed Tenants become children of Workspaces; onboarding entry becomes canonical under Workspace scope.
- Global search becomes workspace-safe (results only within current workspace; no leakage).
If the repository also contains tenant-scoped routes (e.g. `/admin/t/{tenant}/...`), they must be made workspace-safe by ensuring the tenant belongs to the currently selected workspace (or by denying-as-not-found).
Design decisions and rationale are captured in [research.md](research.md).
## Technical Context
<!--
ACTION REQUIRED: Replace the content in this section with the technical details
for the project. The structure here is presented in advisory capacity to guide
the iteration process.
-->
**Language/Version**: PHP 8.4.15
**Primary Dependencies**: Laravel 12, Filament v5, Livewire v4, Tailwind CSS v4
**Storage**: PostgreSQL (via Sail)
**Testing**: Pest v4 (+ PHPUnit v12 underneath)
**Target Platform**: Web application; local dev in Laravel Sail; deploy via containers (Dokploy)
**Project Type**: Web application (Laravel monolith)
**Performance Goals**: Admin UX: workspace-scoped pages should remain responsive (target < 200ms p95 for DB-only requests on typical datasets).
**Constraints**:
- Sail-first execution for all PHP/Artisan/Composer/Node commands.
- No full provider refactor; keep changes scoped to Workspace model, routing/scope, memberships, and managed tenant scoping.
- Preserve deny-as-not-found semantics for non-members; do not leak via global search.
**Scale/Scope**: Multi-workspace, multi-tenant-plane: 1 user may belong to many workspaces; each workspace may contain multiple managed tenants.
## Constitution Check
*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
- Inventory-first: clarify what is last observed vs snapshots/backups
- Read/write separation: any writes require preview + confirmation + audit + tests
- Graph contract path: Graph calls only via `GraphClientInterface` + `config/graph_contracts.php`
- Deterministic capabilities: capability derivation is testable (snapshot/golden tests)
- RBAC-UX: two planes (/admin vs /system) remain separated; cross-plane is 404; non-member tenant access is 404; member-but-missing-capability is 403; authorization checks use Gates/Policies + capability registries (no raw strings, no role-string checks)
- RBAC-UX: destructive-like actions require `->requiresConfirmation()` and clear warning text
- RBAC-UX: global search is tenant-scoped; non-members get no hints; inaccessible results are treated as not found (404 semantics)
- Tenant isolation: all reads/writes tenant-scoped; cross-tenant views are explicit and access-checked
- Run observability: long-running/remote/queued work creates/reuses `OperationRun`; start surfaces enqueue-only; Monitoring is DB-only; DB-only <2s actions may skip runs but security-relevant ones still audit-log; auth handshake exception OPS-EX-AUTH-001 allows synchronous outbound HTTP on `/auth/*` without `OperationRun`
- Automation: queued/scheduled ops use locks + idempotency; handle 429/503 with backoff+jitter
- Data minimization: Inventory stores metadata + whitelisted meta; logs contain no secrets/tokens
- Badge semantics (BADGE-001): status-like badges use `BadgeCatalog` / `BadgeRenderer`; no ad-hoc mappings; new values include tests
**Gate evaluation (pre-Phase 0): PASS (with notes)**
- Inventory-first: Not directly affected (workspace + membership + routing).
- Read/write separation: Workspace/membership mutations are DB-only; require confirmation for destructive-like actions (remove member / demote last owner attempt), emit audit entries, and add tests.
- Graph contract path: No new Graph calls introduced.
- Deterministic capabilities: Extend the canonical capability registry with workspace-plane capabilities; implement deterministic role capability mapping and test it.
- RBAC-UX: Apply the same semantics with Workspace scope:
- Non-member workspace scope deny-as-not-found.
- Member missing capability forbidden.
- UI disabled vs hidden rules remain.
- Global search safety: Introduce workspace-scoped global search query behavior (no results outside current workspace).
- Run observability: Not applicable (no long-running operations planned). Membership changes still require audit logs.
- Badge semantics: Not expected to change for this feature.
## Project Structure
### Documentation (this feature)
```text
specs/[###-feature]/
├── plan.md # This file (/speckit.plan command output)
├── research.md # Phase 0 output (/speckit.plan command)
├── data-model.md # Phase 1 output (/speckit.plan command)
├── quickstart.md # Phase 1 output (/speckit.plan command)
├── contracts/ # Phase 1 output (/speckit.plan command)
└── tasks.md # Phase 2 output (/speckit.tasks command - NOT created by /speckit.plan)
```
### Source Code (repository root)
<!--
ACTION REQUIRED: Replace the placeholder tree below with the concrete layout
for this feature. Delete unused options and expand the chosen structure with
real paths (e.g., apps/admin, packages/something). The delivered plan must
not include Option labels.
-->
```text
app/
├── Filament/
│ ├── Resources/
│ │ ├── WorkspaceResource.php
│ │ └── ...
│ └── Pages/
│ ├── ChooseWorkspace.php
│ └── NoAccess.php
├── Http/
│ └── Middleware/
│ ├── EnsureWorkspaceSelected.php
│ └── EnsureWorkspaceMember.php
├── Models/
│ ├── Workspace.php
│ ├── WorkspaceMembership.php
│ └── Tenant.php (existing; updated to belong to Workspace)
├── Policies/
│ ├── WorkspacePolicy.php
│ └── WorkspaceMembershipPolicy.php
├── Support/
│ ├── Auth/
│ │ ├── Capabilities.php (extend with workspace-plane capabilities)
│ │ └── WorkspaceRole.php (new enum)
│ └── Rbac/
│ ├── UiEnforcement.php (extend or add workspace variant)
│ └── ...
└── Services/
└── Auth/
├── RoleCapabilityMap.php (tenant-plane)
└── WorkspaceRoleCapabilityMap.php (new; workspace-plane)
database/migrations/
tests/Feature/
routes/web.php
bootstrap/app.php (middleware registration; Laravel 12)
bootstrap/providers.php (panel providers; Laravel 11+ rule)
```
**Structure Decision**: Laravel monolith. Implement Workspace scope via middleware + Filament pages/resources under the existing admin panel.
## Phase 0 — Outline & Research (output: research.md)
Completed in [research.md](research.md). Key outcomes:
- URL identity: slug preferred, id fallback.
- Current workspace persistence: session + `users.last_workspace_id`.
- Managed Tenant uniqueness: global unique `entra_tenant_id`.
- Zero memberships: neutral `/admin/no-access` page.
- Workspace-scoped routing + middleware enforcement.
- Global search scoped to active workspace.
- Workspace-level audit stream for membership mutations, implemented as `AuditLog`-compatible entries (stable action IDs; redacted).
## Phase 1 — Design & Contracts
### Data model
See [data-model.md](data-model.md) for entities, constraints, and invariants.
### Contracts
High-level entry-point contract is captured in [contracts/admin-workspaces.openapi.yaml](contracts/admin-workspaces.openapi.yaml).
### Quickstart
Manual verification guidance is in [quickstart.md](quickstart.md).
### Post-design Constitution Re-check
Expected PASS: this feature is DB-only and authorization-heavy; ensure:
- deny-as-not-found for non-members is enforced at middleware + query levels,
- member-without-capability is 403,
- deterministic capability mapping tests exist,
- membership mutations are audited.
## Phase 2 — Implementation Task Preview (for /speckit.tasks)
Planned task groupings:
1) Database migrations + models (workspaces, memberships, user last_workspace_id, tenant workspace_id)
2) Workspace context + middleware + routing (choose-workspace, no-access, `/admin/w/{workspace}` prefix)
3) RBAC/capabilities (capability registry additions + role maps + policies)
4) Filament resources/pages (Workspace CRUD, Members relation manager, managed tenant onboarding/list scoping)
5) Global search scoping (workspace-safe global search queries)
6) Auditing for membership mutations (AuditLog-compatible)
7) Workspace lifecycle (archive/unarchive + selection invalidation)
8) Deterministic capability mapping tests (golden/snapshot)
9) Tests (Pest): last-owner guard, 404/403 semantics, global search scoping, workspace selection flows, migration verification
## Complexity Tracking
> **Fill ONLY if Constitution Check has violations that must be justified**
| Violation | Why Needed | Simpler Alternative Rejected Because |
|-----------|------------|-------------------------------------|
| [e.g., 4th project] | [current need] | [why 3 projects insufficient] |
| [e.g., Repository pattern] | [specific problem] | [why direct DB access insufficient] |