Implements Spec 093 (SCOPE-001) workspace isolation at the data layer. What changed - Adds `workspace_id` to 12 tenant-owned tables and enforces correct binding. - Model write-path enforcement derives workspace from tenant + rejects mismatches. - Prevents `tenant_id` changes (immutability) on tenant-owned records. - Adds queued backfill command + job (`tenantpilot:backfill-workspace-ids`) with OperationRun + AuditLog observability. - Enforces DB constraints (NOT NULL + FK `workspace_id` → `workspaces.id` + composite FK `(tenant_id, workspace_id)` → `tenants(id, workspace_id)`), plus audit_logs invariant. UI / operator visibility - Monitor backfill runs in **Monitoring → Operations** (OperationRun). Tests - `vendor/bin/sail artisan test --compact tests/Feature/WorkspaceIsolation` Notes - Backfill is queued: ensure a queue worker is running (`vendor/bin/sail artisan queue:work`). Spec package - `specs/093-scope-001-workspace-id-isolation/` (plan, tasks, contracts, quickstart, research) Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #112
85 lines
3.8 KiB
Markdown
85 lines
3.8 KiB
Markdown
# Research — 093 SCOPE-001 Workspace ID Isolation
|
||
|
||
**Date**: 2026-02-14
|
||
**Branch**: `093-scope-001-workspace-id-isolation`
|
||
|
||
## Current State (repo evidence)
|
||
|
||
### Workspace/Tenant relationship
|
||
- `workspaces.id` is a Laravel `$table->id()` (bigint).
|
||
- `tenants.workspace_id` exists and is nullable with an index; constraints are applied for non-sqlite drivers.
|
||
|
||
### Tenant-owned tables (target scope)
|
||
The 12 tenant-owned tables currently have `tenant_id` and do **not** have `workspace_id`:
|
||
- `policies` (created 2025_12_10_000110)
|
||
- `policy_versions` (created 2025_12_10_000120)
|
||
- `backup_sets` (created 2025_12_10_000130)
|
||
- `backup_items` (created 2025_12_10_000140)
|
||
- `restore_runs` (created 2025_12_10_000150)
|
||
- `backup_schedules` (created 2026_01_05_011014)
|
||
- `inventory_items` (created 2026_01_07_142720)
|
||
- `inventory_links` (created 2026_01_07_150000)
|
||
- `entra_groups` (created 2026_01_11_120003)
|
||
- `findings` (created 2026_01_13_223311)
|
||
- `entra_role_definitions` (created 2026_02_10_133238)
|
||
- `tenant_permissions` (created 2025_12_11_122423)
|
||
|
||
### Audit logs
|
||
- `audit_logs.tenant_id` is nullable.
|
||
- `audit_logs.workspace_id` exists and is nullable.
|
||
- There is **no** DB-level invariant today preventing `tenant_id != null` with `workspace_id == null`.
|
||
|
||
### Migration patterns already used in the repo
|
||
- Multi-driver migrations (`pgsql`, `mysql`, `sqlite`) exist.
|
||
- SQLite rebuild migrations are used when needed (rename old table, recreate, chunk copy).
|
||
- Postgres/MySQL NOT NULL enforcement is sometimes done with `DB::statement(...)`.
|
||
- Partial unique indexes are used via `DB::statement(...)`.
|
||
|
||
## Decisions
|
||
|
||
### Decision 1 — How to enforce tenant↔workspace consistency
|
||
**Decision**: Use a composite FK for tenant-owned tables on Postgres/MySQL: `(tenant_id, workspace_id)` references `tenants(id, workspace_id)`.
|
||
|
||
**Rationale**:
|
||
- Two independent FKs (`tenant_id → tenants.id` and `workspace_id → workspaces.id`) do not prevent mismatches.
|
||
- A composite FK makes the “workspace derived from tenant” rule enforceable at the DB level, aligning with SCOPE-001’s intent.
|
||
|
||
**Alternatives considered**:
|
||
- App-only validation (insufficient for DB-level isolation goals).
|
||
- Triggers (more complex to deploy/test, harder to reason about).
|
||
- Postgres RLS (high operational cost; broad scope).
|
||
|
||
**Notes/requirements implied**:
|
||
- Add a unique constraint/index on `tenants (id, workspace_id)` (likely with `workspace_id IS NOT NULL`).
|
||
- For SQLite: skip composite FK enforcement (SQLite limitations) while keeping tests green; rely on application enforcement during tests.
|
||
|
||
### Decision 2 — Staged rollout
|
||
**Decision**: Follow the spec’s 4-phase rollout:
|
||
1) Add `workspace_id` nullable columns + indexes.
|
||
2) Enforce write-path assignment + mismatch rejection in the app.
|
||
3) Backfill missing `workspace_id` via an operator command (idempotent, resumable, locked).
|
||
4) Enforce constraints + validate + add final indexes.
|
||
|
||
**Rationale**: Avoid downtime and allow safe production backfill.
|
||
|
||
### Decision 3 — Audit log invariant
|
||
**Decision**: Add a DB check constraint on `audit_logs`:
|
||
- `tenant_id IS NULL OR workspace_id IS NOT NULL`
|
||
|
||
**Rationale**: Directly enforces FR-008 while preserving workspace-only and platform-only events.
|
||
|
||
**Alternative considered**:
|
||
- Enforce in application only (not sufficient for invariants).
|
||
|
||
### Decision 4 — Backfill observability
|
||
**Decision**: The backfill command creates/reuses an `OperationRun` and writes `AuditLog` entries for start/end/outcome.
|
||
|
||
**Rationale**: Matches FR-012 and the constitution’s observability rules for operationally relevant actions.
|
||
|
||
## Open Questions (resolved by spec clarifications)
|
||
- Mismatch handling: reject writes when tenant/workspace mismatch is provided.
|
||
- Invalid mapping during backfill: abort and report.
|
||
- Tenant immutability: reject tenant_id updates.
|
||
- Query/view refactors: out of scope.
|
||
|