TenantAtlas/specs/093-scope-001-workspace-id-isolation/data-model.md
ahmido 92a36ab89e SCOPE-001: DB-level workspace isolation via workspace_id (#112)
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
2026-02-14 22:34:02 +00:00

2.6 KiB

Data Model — 093 SCOPE-001 Workspace ID Isolation

Core Entities

Workspace

  • workspaces:
    • id (bigint)
    • name, slug, timestamps

Tenant

  • tenants:
    • id (bigint)
    • workspace_id (bigint, intended non-null logically; currently nullable in schema)

Ownership rule: A tenant belongs to exactly one workspace; this mapping is the source of truth for deriving workspace bindings.

Tenant-owned Tables (must become workspace-bound)

For each table below:

  • Add workspace_id (bigint FK to workspaces.id)
  • Enforce workspace_id derived from tenant (DB-level composite FK on Postgres/MySQL)
  • Keep tenant_id immutable (application enforcement)

policies

  • Existing: tenant_id, external_id, policy_type, etc.
  • Add: workspace_id

policy_versions

  • Existing: tenant_id, policy_id, snapshot, etc.
  • Add: workspace_id

backup_sets

  • Existing: tenant_id, status/count, etc.
  • Add: workspace_id

backup_items

  • Existing: tenant_id, backup_set_id, payload, etc.
  • Add: workspace_id

restore_runs

  • Existing: tenant_id, backup_set_id, status, preview/results, etc.
  • Add: workspace_id

backup_schedules

  • Existing: tenant_id, enabled/frequency/schedule fields
  • Add: workspace_id

inventory_items

  • Existing: tenant_id, policy identifiers, meta_jsonb, last_seen fields
  • Add: workspace_id
  • Existing: tenant_id, source/target relationship identifiers
  • Add: workspace_id

entra_groups

  • Existing: tenant_id, entra_id, display fields
  • Add: workspace_id

findings

  • Existing: tenant_id, fingerprint, status/severity, run references
  • Add: workspace_id

entra_role_definitions

  • Existing: tenant_id, entra_id, display fields
  • Add: workspace_id

tenant_permissions

  • Existing: tenant_id, permission_key, status
  • Add: workspace_id

Audit Logs (scope invariants)

audit_logs

  • Existing: tenant_id nullable, workspace_id nullable, action/resource fields

Invariant:

  • Tenant-scoped audit entry: tenant_id != null implies workspace_id != null.
  • Workspace-only audit entry: workspace_id != null and tenant_id == null is allowed.
  • Platform-only audit entry: both null is allowed.

Relationship + Constraint Strategy

Tenant-owned enforcement (Postgres/MySQL)

  • Composite FK on each tenant-owned table:
    • (tenant_id, workspace_id) → tenants(id, workspace_id)
  • Standard FK on workspace_id → workspaces.id

SQLite

  • Foreign key / composite constraint enforcement is limited.
  • Testing relies on application enforcement + basic NOT NULL where feasible.