TenantAtlas/specs/093-scope-001-workspace-id-isolation/tasks.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

12 KiB
Raw Blame History

Tasks: 093 — SCOPE-001 Workspace ID Isolation

Input: Design documents from /specs/093-scope-001-workspace-id-isolation/
Prerequisites: plan.md (required), spec.md (required for user stories), research.md, data-model.md, contracts/, quickstart.md
Tests: For runtime behavior changes in this repo, tests are REQUIRED (Pest). Only docs-only changes may omit tests. Operations: This feature introduces an operator command (long-running), so tasks include creating/reusing and updating a canonical OperationRun and creating AuditLog entries before/after.

Organization: Tasks are grouped by user story to enable independent implementation and testing of each story.

Format: [ID] [P?] [Story] Description

  • [P]: Can run in parallel (different files, no dependencies)
  • [Story]: Which user story this task belongs to (e.g., US1, US2, US3)
  • Include file paths in descriptions

Path Conventions

  • Laravel app code: app/
  • Migrations: database/migrations/
  • Pest tests: tests/Feature/

Phase 1: Setup (Shared Infrastructure)

Purpose: Confirm design inputs + existing code entrypoints

  • T001 Verify feature docs are present and consistent in specs/093-scope-001-workspace-id-isolation/{spec.md,plan.md,research.md,data-model.md,contracts/,quickstart.md,tasks.md}
  • T002 [P] Inventory target models exist for the 12 tables in app/Models/{Policy,PolicyVersion,BackupSet,BackupItem,RestoreRun,BackupSchedule,InventoryItem,InventoryLink,EntraGroup,Finding,EntraRoleDefinition,TenantPermission}.php
  • T003 [P] Identify audit logging entrypoints that must set workspace_id in app/Services/Intune/AuditLogger.php and app/Services/Audit/WorkspaceAuditLogger.php

Phase 2: Foundational (Blocking Prerequisites)

Purpose: Shared building blocks used across all stories

⚠️ CRITICAL: No user story work can begin until this phase is complete

  • T004 Create shared model concern for workspace binding + tenant immutability in app/Support/Concerns/DerivesWorkspaceIdFromTenant.php
  • T005 [P] Add supporting exception type for mismatch/immutability errors in app/Support/WorkspaceIsolation/WorkspaceIsolationViolation.php
  • T006 Add a small list of tenant-owned table names for reuse (command + tests) in app/Support/WorkspaceIsolation/TenantOwnedTables.php

Checkpoint: Foundation ready (shared enforcement building blocks exist)


Phase 3: User Story 1 — Enforce workspace ownership at the data layer (Priority: P1) 🎯 MVP

Goal: Every tenant-owned record becomes explicitly workspace-bound, and the system prevents new tenant-owned writes without a correct workspace binding.

Independent Test:

  • Creating a tenant-owned record without workspace_id results in workspace_id being derived from tenant.
  • Creating/updating a tenant-owned record with a mismatched workspace_id is rejected.

Tests for User Story 1

NOTE: Write these tests first and ensure they fail before implementation.

  • T007 [P] [US1] Add unit-level enforcement tests for the shared concern in tests/Feature/WorkspaceIsolation/DerivesWorkspaceIdFromTenantTest.php
  • T007a [P] [US1] Add immutability test case (attempt to change tenant_id is rejected) in tests/Feature/WorkspaceIsolation/DerivesWorkspaceIdFromTenantTest.php

Implementation for User Story 1

  • T008 [P] [US1] Add nullable workspace_id + indexes to policies in database/migrations/*_add_workspace_id_to_policies_table.php

  • T009 [P] [US1] Add nullable workspace_id + indexes to policy_versions in database/migrations/*_add_workspace_id_to_policy_versions_table.php

  • T010 [P] [US1] Add nullable workspace_id + indexes to backup_sets in database/migrations/*_add_workspace_id_to_backup_sets_table.php

  • T011 [P] [US1] Add nullable workspace_id + indexes to backup_items in database/migrations/*_add_workspace_id_to_backup_items_table.php

  • T012 [P] [US1] Add nullable workspace_id + indexes to restore_runs in database/migrations/*_add_workspace_id_to_restore_runs_table.php

  • T013 [P] [US1] Add nullable workspace_id + indexes to backup_schedules in database/migrations/*_add_workspace_id_to_backup_schedules_table.php

  • T014 [P] [US1] Add nullable workspace_id + indexes to inventory_items in database/migrations/*_add_workspace_id_to_inventory_items_table.php

  • T015 [P] [US1] Add nullable workspace_id + indexes to inventory_links in database/migrations/*_add_workspace_id_to_inventory_links_table.php

  • T016 [P] [US1] Add nullable workspace_id + indexes to entra_groups in database/migrations/*_add_workspace_id_to_entra_groups_table.php

  • T017 [P] [US1] Add nullable workspace_id + indexes to findings in database/migrations/*_add_workspace_id_to_findings_table.php

  • T018 [P] [US1] Add nullable workspace_id + indexes to entra_role_definitions in database/migrations/*_add_workspace_id_to_entra_role_definitions_table.php

  • T019 [P] [US1] Add nullable workspace_id + indexes to tenant_permissions in database/migrations/*_add_workspace_id_to_tenant_permissions_table.php

  • T019a [US1] Enforce tenant_id immutability in app/Support/Concerns/DerivesWorkspaceIdFromTenant.php (reject updates when tenant_id differs from original)

  • T020 [P] [US1] Apply DerivesWorkspaceIdFromTenant concern to Policy model in app/Models/Policy.php

  • T021 [P] [US1] Apply DerivesWorkspaceIdFromTenant concern to PolicyVersion model in app/Models/PolicyVersion.php

  • T022 [P] [US1] Apply DerivesWorkspaceIdFromTenant concern to BackupSet model in app/Models/BackupSet.php

  • T023 [P] [US1] Apply DerivesWorkspaceIdFromTenant concern to BackupItem model in app/Models/BackupItem.php

  • T024 [P] [US1] Apply DerivesWorkspaceIdFromTenant concern to RestoreRun model in app/Models/RestoreRun.php

  • T025 [P] [US1] Apply DerivesWorkspaceIdFromTenant concern to BackupSchedule model in app/Models/BackupSchedule.php

  • T026 [P] [US1] Apply DerivesWorkspaceIdFromTenant concern to InventoryItem model in app/Models/InventoryItem.php

  • T027 [P] [US1] Apply DerivesWorkspaceIdFromTenant concern to InventoryLink model in app/Models/InventoryLink.php

  • T028 [P] [US1] Apply DerivesWorkspaceIdFromTenant concern to EntraGroup model in app/Models/EntraGroup.php

  • T029 [P] [US1] Apply DerivesWorkspaceIdFromTenant concern to Finding model in app/Models/Finding.php

  • T030 [P] [US1] Apply DerivesWorkspaceIdFromTenant concern to EntraRoleDefinition model in app/Models/EntraRoleDefinition.php

  • T031 [P] [US1] Apply DerivesWorkspaceIdFromTenant concern to TenantPermission model in app/Models/TenantPermission.php

  • T032 [US1] Add tenants composite uniqueness to support composite FK in database/migrations/*_add_tenants_id_workspace_id_unique.php

Post-backfill constraints (depends on US2 completion):

  • T033 [US1] Enforce NOT NULL workspace_id for the 12 tenant-owned tables in database/migrations/*_enforce_workspace_id_not_null_on_tenant_owned_tables.php
  • T034 [US1] Add FK workspace_id → workspaces.id + composite FK (tenant_id, workspace_id) → tenants(id, workspace_id) for the 12 tables in database/migrations/*_add_workspace_isolation_constraints_to_tenant_owned_tables.php

Checkpoint: US1 complete once new writes are safe and DB constraints are enforceable after backfill.


Phase 4: User Story 2 — Safely backfill existing production data (Priority: P2)

Goal: Operators can safely backfill missing workspace_id across all 12 tables without downtime.

Independent Test:

  • With seeded rows missing workspace_id, running the command sets workspace_id correctly.
  • Re-running the command is idempotent.
  • If a tenant→workspace mapping cannot be resolved, the command aborts and reports.

Tests for User Story 2

NOTE: Write these tests first and ensure they fail before implementation.

  • T035 [P] [US2] Add backfill command tests in tests/Feature/WorkspaceIsolation/BackfillWorkspaceIdsCommandTest.php

Implementation for User Story 2

  • T036 [US2] Implement operator command skeleton + options in app/Console/Commands/TenantpilotBackfillWorkspaceIds.php
  • T036a [US2] Implement queued job runner for batch/table backfills in app/Jobs/BackfillWorkspaceIdsJob.php
  • T036b [US2] Dispatch jobs from app/Console/Commands/TenantpilotBackfillWorkspaceIds.php and ensure “start → dispatch → view run” flow
  • T037 [US2] Add concurrency lock (Cache::lock) to prevent concurrent backfills in app/Console/Commands/TenantpilotBackfillWorkspaceIds.php
  • T038 [US2] Implement dry-run counts + per-table reporting in app/Console/Commands/TenantpilotBackfillWorkspaceIds.php
  • T039 [US2] Implement per-workspace OperationRun creation in app/Console/Commands/TenantpilotBackfillWorkspaceIds.php using app/Services/OperationRunService.php (ensureWorkspaceRunWithIdentity)
  • T040 [US2] Write start/end/outcome AuditLog summaries per workspace in app/Console/Commands/TenantpilotBackfillWorkspaceIds.php using app/Services/Audit/WorkspaceAuditLogger.php
  • T041 [US2] Implement backfill updates for all 12 tables in app/Jobs/BackfillWorkspaceIdsJob.php (UPDATE ... FROM tenants WHERE workspace_id IS NULL)
  • T042 [US2] Implement abort-and-report behavior when tenant workspace_id is missing/unresolvable in app/Console/Commands/TenantpilotBackfillWorkspaceIds.php
  • T043 [US2] Implement progress tracking (counts + last processed id) persisted into OperationRun context from app/Jobs/BackfillWorkspaceIdsJob.php

Checkpoint: US2 complete when backfill is safe to run repeatedly and produces OperationRun + AuditLog observability.


Phase 5: User Story 3 — Make audit logs unambiguous across scopes (Priority: P3)

Goal: If an audit log entry references a tenant, it must also reference a workspace.

Independent Test:

  • Tenant-scoped audit logs always store workspace_id.
  • DB prevents tenant_id set with workspace_id null.
  • Workspace-only and platform-only logs remain allowed.

Tests for User Story 3

NOTE: Write these tests first and ensure they fail before implementation.

  • T044 [P] [US3] Add audit invariant tests for tenant/workspace/platform scopes in tests/Feature/WorkspaceIsolation/AuditLogScopeInvariantTest.php

Implementation for User Story 3

  • T045 [US3] Ensure tenant-scoped audit writes include workspace_id in app/Services/Intune/AuditLogger.php
  • T046 [US3] Add migration to backfill audit_logs.workspace_id where tenant_id is present (join tenants) in database/migrations/*_backfill_workspace_id_on_audit_logs.php
  • T047 [US3] Add check constraint enforcing tenant_id IS NULL OR workspace_id IS NOT NULL in database/migrations/*_add_audit_logs_scope_check_constraint.php

Checkpoint: US3 complete when invariant is enforced in both app writes and DB.


Phase 6: Polish & Cross-Cutting Concerns

  • T048 [P] Add validation SQL snippets for operators in specs/093-scope-001-workspace-id-isolation/quickstart.md
  • T049 Ensure tasks and rollout order remain accurate after implementation changes in specs/093-scope-001-workspace-id-isolation/plan.md

Dependencies & Execution Order

Story order

  • Setup (Phase 1) → Foundational (Phase 2) → US1 (Phase 3) → US2 (Phase 4) → US1 constraints (T033T034) → US3 (Phase 5) → Polish

Dependency graph (story-level)

  • US1 (nullable columns + app enforcement) → blocks US2 (backfill)
  • US2 (backfill) → blocks US1 constraints (T033T034)
  • US3 can be started after Foundational, but DB check constraint should land after US2 backfill if historical audit_logs need repair first.

Parallel execution examples

Parallel Example: US1

  • [P] T008T019 can run in parallel (independent migrations per table)
  • [P] T020T031 can run in parallel (independent model updates)

Parallel Example: US2

  • [P] T035 (tests) can be written while T036T038 (command skeleton + reporting) are implemented

Parallel Example: US3

  • [P] T044 (tests) can be written while T045T047 are implemented

Implementation Strategy

MVP First (US1 only)

  1. Complete Phase 1 (Setup)
  2. Complete Phase 2 (Foundational)
  3. Complete US1 through app enforcement + nullable columns (T007T032)
  4. STOP and validate US1 tests pass independently

Incremental Delivery

  1. Add US2 (backfill) and validate idempotency + observability (T035T043)
  2. Enforce US1 post-backfill DB constraints (T033T034)
  3. Add US3 audit invariant (T044T047)
  4. Final polish/runbook validation (T048T049)