# 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 (T033–T034) → US3 (Phase 5) → Polish

### Dependency graph (story-level)

- US1 (nullable columns + app enforcement) → blocks US2 (backfill)
- US2 (backfill) → blocks US1 constraints (T033–T034)
- 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] T008–T019 can run in parallel (independent migrations per table)
- [P] T020–T031 can run in parallel (independent model updates)

### Parallel Example: US2

- [P] T035 (tests) can be written while T036–T038 (command skeleton + reporting) are implemented

### Parallel Example: US3

- [P] T044 (tests) can be written while T045–T047 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 (T007–T032)
4. STOP and validate US1 tests pass independently

### Incremental Delivery

1. Add US2 (backfill) and validate idempotency + observability (T035–T043)
2. Enforce US1 post-backfill DB constraints (T033–T034)
3. Add US3 audit invariant (T044–T047)
4. Final polish/runbook validation (T048–T049)
