193 lines
9.7 KiB
Markdown
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] |
|