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

211 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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
- [X] 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}
- [X] 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
- [X] 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
- [X] T004 Create shared model concern for workspace binding + tenant immutability in app/Support/Concerns/DerivesWorkspaceIdFromTenant.php
- [X] T005 [P] Add supporting exception type for mismatch/immutability errors in app/Support/WorkspaceIsolation/WorkspaceIsolationViolation.php
- [X] 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.
- [X] T007 [P] [US1] Add unit-level enforcement tests for the shared concern in tests/Feature/WorkspaceIsolation/DerivesWorkspaceIdFromTenantTest.php
- [X] 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
- [X] T008 [P] [US1] Add nullable workspace_id + indexes to policies in database/migrations/*_add_workspace_id_to_policies_table.php
- [X] T009 [P] [US1] Add nullable workspace_id + indexes to policy_versions in database/migrations/*_add_workspace_id_to_policy_versions_table.php
- [X] T010 [P] [US1] Add nullable workspace_id + indexes to backup_sets in database/migrations/*_add_workspace_id_to_backup_sets_table.php
- [X] T011 [P] [US1] Add nullable workspace_id + indexes to backup_items in database/migrations/*_add_workspace_id_to_backup_items_table.php
- [X] T012 [P] [US1] Add nullable workspace_id + indexes to restore_runs in database/migrations/*_add_workspace_id_to_restore_runs_table.php
- [X] T013 [P] [US1] Add nullable workspace_id + indexes to backup_schedules in database/migrations/*_add_workspace_id_to_backup_schedules_table.php
- [X] T014 [P] [US1] Add nullable workspace_id + indexes to inventory_items in database/migrations/*_add_workspace_id_to_inventory_items_table.php
- [X] T015 [P] [US1] Add nullable workspace_id + indexes to inventory_links in database/migrations/*_add_workspace_id_to_inventory_links_table.php
- [X] T016 [P] [US1] Add nullable workspace_id + indexes to entra_groups in database/migrations/*_add_workspace_id_to_entra_groups_table.php
- [X] T017 [P] [US1] Add nullable workspace_id + indexes to findings in database/migrations/*_add_workspace_id_to_findings_table.php
- [X] T018 [P] [US1] Add nullable workspace_id + indexes to entra_role_definitions in database/migrations/*_add_workspace_id_to_entra_role_definitions_table.php
- [X] T019 [P] [US1] Add nullable workspace_id + indexes to tenant_permissions in database/migrations/*_add_workspace_id_to_tenant_permissions_table.php
- [X] T019a [US1] Enforce tenant_id immutability in app/Support/Concerns/DerivesWorkspaceIdFromTenant.php (reject updates when tenant_id differs from original)
- [X] T020 [P] [US1] Apply DerivesWorkspaceIdFromTenant concern to Policy model in app/Models/Policy.php
- [X] T021 [P] [US1] Apply DerivesWorkspaceIdFromTenant concern to PolicyVersion model in app/Models/PolicyVersion.php
- [X] T022 [P] [US1] Apply DerivesWorkspaceIdFromTenant concern to BackupSet model in app/Models/BackupSet.php
- [X] T023 [P] [US1] Apply DerivesWorkspaceIdFromTenant concern to BackupItem model in app/Models/BackupItem.php
- [X] T024 [P] [US1] Apply DerivesWorkspaceIdFromTenant concern to RestoreRun model in app/Models/RestoreRun.php
- [X] T025 [P] [US1] Apply DerivesWorkspaceIdFromTenant concern to BackupSchedule model in app/Models/BackupSchedule.php
- [X] T026 [P] [US1] Apply DerivesWorkspaceIdFromTenant concern to InventoryItem model in app/Models/InventoryItem.php
- [X] T027 [P] [US1] Apply DerivesWorkspaceIdFromTenant concern to InventoryLink model in app/Models/InventoryLink.php
- [X] T028 [P] [US1] Apply DerivesWorkspaceIdFromTenant concern to EntraGroup model in app/Models/EntraGroup.php
- [X] T029 [P] [US1] Apply DerivesWorkspaceIdFromTenant concern to Finding model in app/Models/Finding.php
- [X] T030 [P] [US1] Apply DerivesWorkspaceIdFromTenant concern to EntraRoleDefinition model in app/Models/EntraRoleDefinition.php
- [X] T031 [P] [US1] Apply DerivesWorkspaceIdFromTenant concern to TenantPermission model in app/Models/TenantPermission.php
- [X] 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):**
- [X] 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
- [X] 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.
- [X] T035 [P] [US2] Add backfill command tests in tests/Feature/WorkspaceIsolation/BackfillWorkspaceIdsCommandTest.php
### Implementation for User Story 2
- [X] T036 [US2] Implement operator command skeleton + options in app/Console/Commands/TenantpilotBackfillWorkspaceIds.php
- [X] T036a [US2] Implement queued job runner for batch/table backfills in app/Jobs/BackfillWorkspaceIdsJob.php
- [X] T036b [US2] Dispatch jobs from app/Console/Commands/TenantpilotBackfillWorkspaceIds.php and ensure “start → dispatch → view run” flow
- [X] T037 [US2] Add concurrency lock (Cache::lock) to prevent concurrent backfills in app/Console/Commands/TenantpilotBackfillWorkspaceIds.php
- [X] T038 [US2] Implement dry-run counts + per-table reporting in app/Console/Commands/TenantpilotBackfillWorkspaceIds.php
- [X] T039 [US2] Implement per-workspace OperationRun creation in app/Console/Commands/TenantpilotBackfillWorkspaceIds.php using app/Services/OperationRunService.php (ensureWorkspaceRunWithIdentity)
- [X] T040 [US2] Write start/end/outcome AuditLog summaries per workspace in app/Console/Commands/TenantpilotBackfillWorkspaceIds.php using app/Services/Audit/WorkspaceAuditLogger.php
- [X] T041 [US2] Implement backfill updates for all 12 tables in app/Jobs/BackfillWorkspaceIdsJob.php (UPDATE ... FROM tenants WHERE workspace_id IS NULL)
- [X] T042 [US2] Implement abort-and-report behavior when tenant workspace_id is missing/unresolvable in app/Console/Commands/TenantpilotBackfillWorkspaceIds.php
- [X] 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.
- [X] T044 [P] [US3] Add audit invariant tests for tenant/workspace/platform scopes in tests/Feature/WorkspaceIsolation/AuditLogScopeInvariantTest.php
### Implementation for User Story 3
- [X] T045 [US3] Ensure tenant-scoped audit writes include workspace_id in app/Services/Intune/AuditLogger.php
- [X] 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
- [X] 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
- [X] T048 [P] Add validation SQL snippets for operators in specs/093-scope-001-workspace-id-isolation/quickstart.md
- [X] 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)