TenantAtlas/specs/093-scope-001-workspace-id-isolation/research.md
2026-02-14 22:32:38 +01:00

3.8 KiB
Raw Blame History

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-001s 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 specs 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 constitutions 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.