3.8 KiB
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.idis a Laravel$table->id()(bigint).tenants.workspace_idexists 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_idis nullable.audit_logs.workspace_idexists and is nullable.- There is no DB-level invariant today preventing
tenant_id != nullwithworkspace_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.idandworkspace_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 withworkspace_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:
- Add
workspace_idnullable columns + indexes. - Enforce write-path assignment + mismatch rejection in the app.
- Backfill missing
workspace_idvia an operator command (idempotent, resumable, locked). - 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.