spec: scope-001 workspace isolation
This commit is contained in:
parent
3ddf8c3fd6
commit
f8763a8f26
@ -0,0 +1,34 @@
|
||||
# Specification Quality Checklist: SCOPE-001 Workspace ID Isolation
|
||||
|
||||
**Purpose**: Validate specification completeness and quality before proceeding to planning
|
||||
**Created**: 2026-02-14
|
||||
**Feature**: ./spec.md
|
||||
|
||||
## Content Quality
|
||||
|
||||
- [x] No implementation details (languages, frameworks, APIs)
|
||||
- [x] Focused on user value and business needs
|
||||
- [x] Written for non-technical stakeholders
|
||||
- [x] All mandatory sections completed
|
||||
|
||||
## Requirement Completeness
|
||||
|
||||
- [x] No [NEEDS CLARIFICATION] markers remain
|
||||
- [x] Requirements are testable and unambiguous
|
||||
- [x] Success criteria are measurable
|
||||
- [x] Success criteria are technology-agnostic (no implementation details)
|
||||
- [x] All acceptance scenarios are defined
|
||||
- [x] Edge cases are identified
|
||||
- [x] Scope is clearly bounded
|
||||
- [x] Dependencies and assumptions identified
|
||||
|
||||
## Feature Readiness
|
||||
|
||||
- [x] All functional requirements have clear acceptance criteria
|
||||
- [x] User scenarios cover primary flows
|
||||
- [x] Feature meets measurable outcomes defined in Success Criteria
|
||||
- [x] No implementation details leak into specification
|
||||
|
||||
## Notes
|
||||
|
||||
- All checklist items pass; spec is ready for `/speckit.plan`.
|
||||
48
specs/093-scope-001-workspace-id-isolation/contracts/cli.md
Normal file
48
specs/093-scope-001-workspace-id-isolation/contracts/cli.md
Normal file
@ -0,0 +1,48 @@
|
||||
# CLI Contract — 093 Workspace ID Backfill
|
||||
|
||||
This feature adds an operator-only Artisan command to backfill missing `workspace_id` on tenant-owned tables.
|
||||
|
||||
## Command
|
||||
|
||||
- Name (proposed): `tenantpilot:backfill-workspace-ids`
|
||||
|
||||
## Flags / Options (proposed)
|
||||
|
||||
- `--dry-run` (default: false)
|
||||
- Prints counts per table and exits without writing.
|
||||
- `--table=<name>` (optional)
|
||||
- Restrict execution to a single table.
|
||||
- `--batch-size=<n>` (default: 5_000)
|
||||
- Batch size for updates (where chunking is used).
|
||||
- `--resume-from=<cursor>` (optional)
|
||||
- Resume from a saved cursor/checkpoint (implementation-defined).
|
||||
- `--max-rows=<n>` (optional)
|
||||
- Safety valve for partial runs.
|
||||
|
||||
## Safety + Observability
|
||||
|
||||
Execution strategy (queued):
|
||||
|
||||
- The command is a start surface only: authorize → acquire lock → create/reuse `OperationRun` → dispatch queued jobs → print a “View run” pointer.
|
||||
- The backfill mutations MUST execute inside queued jobs (batch/table scoped) to support large datasets.
|
||||
|
||||
Safety + observability requirements:
|
||||
|
||||
- Must acquire a lock (cache/DB-backed lock) to prevent concurrent runs.
|
||||
- Must create/reuse an `OperationRun` for visibility and progress tracking.
|
||||
- Must write an `AuditLog` entry for start and end (outcome, counts, duration).
|
||||
- Must abort and report when a tenant→workspace mapping cannot be resolved.
|
||||
|
||||
## Output
|
||||
|
||||
- Printed “Run started” summary:
|
||||
- `OperationRun` identifier (or URL/route reference when available)
|
||||
- jobs dispatched count
|
||||
- selected tables / scope
|
||||
|
||||
- Per-table totals:
|
||||
- scanned rows
|
||||
- rows missing `workspace_id`
|
||||
- rows updated
|
||||
- Final summary + recommended validation SQL.
|
||||
|
||||
@ -0,0 +1,10 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: TenantPilot — Spec 093 Contracts
|
||||
version: 1.0.0
|
||||
description: |
|
||||
Spec 093 introduces no new HTTP routes.
|
||||
|
||||
This OpenAPI file is intentionally minimal to document that the rollout
|
||||
is implemented via database migrations + an operator-only Artisan command.
|
||||
paths: {}
|
||||
92
specs/093-scope-001-workspace-id-isolation/data-model.md
Normal file
92
specs/093-scope-001-workspace-id-isolation/data-model.md
Normal file
@ -0,0 +1,92 @@
|
||||
# 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`
|
||||
|
||||
### inventory_links
|
||||
- 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.
|
||||
|
||||
155
specs/093-scope-001-workspace-id-isolation/plan.md
Normal file
155
specs/093-scope-001-workspace-id-isolation/plan.md
Normal file
@ -0,0 +1,155 @@
|
||||
# Implementation Plan: Spec 093 — SCOPE-001 Workspace ID Isolation
|
||||
|
||||
**Branch**: `093-scope-001-workspace-id-isolation` | **Date**: 2026-02-14
|
||||
**Spec**: `specs/093-scope-001-workspace-id-isolation/spec.md`
|
||||
**Spec (absolute)**: `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/093-scope-001-workspace-id-isolation/spec.md`
|
||||
**Input**: `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/093-scope-001-workspace-id-isolation/spec.md`
|
||||
|
||||
## Summary
|
||||
|
||||
Enforce DB-level workspace isolation for tenant-owned data by adding `workspace_id` to 12 tenant-owned tables, safely backfilling legacy rows, and then enforcing NOT NULL + referential integrity.
|
||||
|
||||
Additionally, fix the audit trail invariant: if an `audit_logs` entry references a tenant, it must also reference a workspace.
|
||||
|
||||
Rollout is staged to avoid downtime:
|
||||
1) Add nullable `workspace_id` columns.
|
||||
2) Enforce write-path derivation + mismatch rejection.
|
||||
3) Backfill in batches with resumability, locking, and observability (`OperationRun` + `AuditLog`).
|
||||
4) Enforce constraints and add final indexes.
|
||||
|
||||
## Technical Context
|
||||
|
||||
**Language/Version**: PHP 8.4 (Laravel 12)
|
||||
**Primary Dependencies**: Filament v5, Livewire v4, Laravel Sail, Tailwind CSS v4
|
||||
**Storage**: PostgreSQL (primary), with SQLite support patterns used in migrations for tests/CI
|
||||
**Testing**: Pest v4 (`vendor/bin/sail artisan test --compact`)
|
||||
**Target Platform**: Web (admin SaaS)
|
||||
**Project Type**: Laravel monolith (Filament panels + Livewire + Artisan commands)
|
||||
**Performance Goals**:
|
||||
- Backfill updates run in batches to avoid long locks.
|
||||
- Postgres uses `CONCURRENTLY` for large index creation where applicable.
|
||||
**Constraints**:
|
||||
- No new HTTP routes/pages.
|
||||
- No planned downtime; staged rollout.
|
||||
- Backfill is idempotent, resumable, and aborts on tenant→workspace mapping failures.
|
||||
**Scale/Scope**: Potentially large datasets (unknown upper bound); plan assumes millions of rows are possible across inventory/backup/history tables.
|
||||
|
||||
## Constitution Check
|
||||
|
||||
*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
|
||||
|
||||
- Inventory-first / snapshots: PASS (schema-only + backfill; no changes to inventory/snapshot semantics).
|
||||
- Read/write separation: PASS (writes are limited to migrations + operator backfill; no UI “write surfaces” are added).
|
||||
- Graph contract path: PASS (no Graph calls).
|
||||
- Deterministic capabilities: PASS (no capability resolver changes).
|
||||
- Workspace isolation: PASS (strengthens isolation by enforcing workspace binding at the data layer).
|
||||
- Tenant isolation: PASS (tenant-owned tables remain tenant-scoped; DB constraints prevent cross-workspace mismatches).
|
||||
- RBAC-UX / planes: PASS (no changes to `/admin` vs `/system`; no new access surfaces).
|
||||
- Run observability: PASS (backfill is operationally relevant and will be tracked via `OperationRun` + `AuditLog`).
|
||||
- Filament Action Surface Contract: N/A (no Filament Resource/Page changes).
|
||||
|
||||
## Project Structure
|
||||
|
||||
### Documentation (this feature)
|
||||
|
||||
```text
|
||||
specs/093-scope-001-workspace-id-isolation/
|
||||
├── plan.md
|
||||
├── research.md
|
||||
├── data-model.md
|
||||
├── quickstart.md
|
||||
├── contracts/
|
||||
│ ├── openapi.yaml
|
||||
│ └── cli.md
|
||||
└── tasks.md
|
||||
```
|
||||
|
||||
### Source Code (repository root)
|
||||
|
||||
```text
|
||||
app/
|
||||
├── Console/
|
||||
│ └── Commands/
|
||||
├── Models/
|
||||
└── Support/ (or Services/)
|
||||
|
||||
database/
|
||||
└── migrations/
|
||||
|
||||
tests/
|
||||
└── Feature/
|
||||
```
|
||||
|
||||
**Structure Decision**: Implement as Laravel migrations + an Artisan operator command + model-level enforcement helpers, with Pest feature tests.
|
||||
|
||||
## Phase Plan
|
||||
|
||||
### Phase 0 — Research (complete)
|
||||
|
||||
Outputs:
|
||||
- `specs/093-scope-001-workspace-id-isolation/research.md`
|
||||
|
||||
Key decisions captured:
|
||||
- Tenant↔workspace consistency will be enforced with composite FKs on Postgres/MySQL.
|
||||
- Audit invariant enforced with a check constraint.
|
||||
|
||||
### Phase 1 — Design & Contracts (complete)
|
||||
|
||||
Outputs:
|
||||
- `specs/093-scope-001-workspace-id-isolation/data-model.md`
|
||||
- `specs/093-scope-001-workspace-id-isolation/contracts/openapi.yaml` (no new routes)
|
||||
- `specs/093-scope-001-workspace-id-isolation/contracts/cli.md` (Artisan backfill contract)
|
||||
- `specs/093-scope-001-workspace-id-isolation/quickstart.md`
|
||||
|
||||
**Post-design constitution re-check**: PASS (no new external calls; operational backfill is observable).
|
||||
|
||||
### Phase 2 — Implementation Planning (next)
|
||||
|
||||
Implementation will be delivered as small, test-driven slices aligned to the staged rollout.
|
||||
|
||||
1) Phase 1 migrations — add nullable `workspace_id`
|
||||
- Add `workspace_id` (nullable) + index to the 12 tenant-owned tables.
|
||||
- Add baseline scoping indexes for expected query patterns (at minimum `workspace_id` and `(workspace_id, tenant_id)` where useful).
|
||||
- Ensure migrations follow existing multi-driver patterns (SQLite fallbacks where needed).
|
||||
|
||||
2) Phase 1.5 — write-path enforcement (application)
|
||||
- For each affected model/write path:
|
||||
- On create: derive `workspace_id` from `tenant.workspace_id`.
|
||||
- On update: reject changes to `tenant_id` (immutability) and reject explicit workspace mismatches.
|
||||
- Ensure audit log writer sets `workspace_id` when `tenant_id` is present.
|
||||
|
||||
3) Phase 2 — backfill command (operator-only)
|
||||
- Add `tenantpilot:backfill-workspace-ids` (name TBD).
|
||||
- Safety requirements:
|
||||
- Acquire lock to prevent concurrent execution.
|
||||
- Batch updates per table and allow resume/checkpoint.
|
||||
- Abort and report table + sample IDs if a tenant→workspace mapping cannot be resolved.
|
||||
- Observability:
|
||||
- Create/reuse an `OperationRun` describing the backfill run.
|
||||
- Write `AuditLog` summary entries for start/end/outcome.
|
||||
|
||||
- Execution strategy (queued):
|
||||
- The command MUST be a lightweight start surface: authorize → acquire lock → create/reuse OperationRun → dispatch queued jobs → print a “View run” pointer.
|
||||
- The actual backfill mutations MUST execute inside queued jobs (batch/table scoped) so large datasets do not require a single long-running synchronous CLI process.
|
||||
- Jobs MUST update OperationRun progress/counters and record failures with stable reason codes + sanitized messages.
|
||||
|
||||
4) Phase 3 — constraints + validation + final indexes
|
||||
- Tenant-owned tables:
|
||||
- Set `workspace_id` to NOT NULL (after validation).
|
||||
- Add FK `workspace_id → workspaces.id`.
|
||||
- Add composite FK `(tenant_id, workspace_id) → tenants(id, workspace_id)` on Postgres/MySQL.
|
||||
- For Postgres, prefer `NOT VALID` then `VALIDATE CONSTRAINT` to reduce lock time.
|
||||
- Tenants:
|
||||
- Add a unique constraint/index on `(id, workspace_id)` to support composite FKs.
|
||||
- Audit logs:
|
||||
- Backfill `workspace_id` for rows where `tenant_id` is present.
|
||||
- Add check constraint: `tenant_id IS NULL OR workspace_id IS NOT NULL`.
|
||||
- Index strategy:
|
||||
- Use `CREATE INDEX CONCURRENTLY` on Postgres for large tables (migrations must not run in a transaction).
|
||||
|
||||
5) Pest tests (minimal, high-signal)
|
||||
- Backfill correctness on a representative table (seed missing `workspace_id`, run backfill, assert set).
|
||||
- DB constraint tests (where supported by test DB):
|
||||
- `tenant_id` + mismatched `workspace_id` cannot be persisted after Phase 3 constraints.
|
||||
- audit invariant: tenant-scoped audit requires workspace; workspace-only and platform-only are allowed.
|
||||
|
||||
56
specs/093-scope-001-workspace-id-isolation/quickstart.md
Normal file
56
specs/093-scope-001-workspace-id-isolation/quickstart.md
Normal file
@ -0,0 +1,56 @@
|
||||
# Quickstart — 093 Workspace ID Isolation
|
||||
|
||||
## Goal
|
||||
|
||||
Run the staged rollout locally/staging to ensure all tenant-owned tables are workspace-bound and audit invariants hold.
|
||||
|
||||
## Prereqs
|
||||
|
||||
- Sail is running: `vendor/bin/sail up -d`
|
||||
- DB is migrated: `vendor/bin/sail artisan migrate`
|
||||
|
||||
## Rollout Order
|
||||
|
||||
1) **Phase 1** — Add nullable `workspace_id` columns + indexes
|
||||
- Deploy migrations
|
||||
|
||||
2) **Phase 1.5** — Deploy app write-path enforcement
|
||||
- New/updated tenant-owned writes derive `workspace_id` from `tenant.workspace_id`
|
||||
- Mismatches are rejected
|
||||
|
||||
3) **Phase 2** — Backfill existing rows
|
||||
- Dry-run:
|
||||
- `vendor/bin/sail artisan tenantpilot:backfill-workspace-ids --dry-run`
|
||||
- Execute:
|
||||
- `vendor/bin/sail artisan tenantpilot:backfill-workspace-ids`
|
||||
|
||||
4) **Phase 3** — Enforce constraints + validate + final indexes
|
||||
- Apply NOT NULL + FKs + composite FKs
|
||||
- Add audit_logs check constraint
|
||||
|
||||
## Validation SQL (Postgres)
|
||||
|
||||
Run these to confirm no missing bindings remain.
|
||||
|
||||
Tenant-owned tables:
|
||||
- `SELECT count(*) FROM policies WHERE workspace_id IS NULL;`
|
||||
- `SELECT count(*) FROM policy_versions WHERE workspace_id IS NULL;`
|
||||
- `SELECT count(*) FROM backup_sets WHERE workspace_id IS NULL;`
|
||||
- `SELECT count(*) FROM backup_items WHERE workspace_id IS NULL;`
|
||||
- `SELECT count(*) FROM restore_runs WHERE workspace_id IS NULL;`
|
||||
- `SELECT count(*) FROM backup_schedules WHERE workspace_id IS NULL;`
|
||||
- `SELECT count(*) FROM inventory_items WHERE workspace_id IS NULL;`
|
||||
- `SELECT count(*) FROM inventory_links WHERE workspace_id IS NULL;`
|
||||
- `SELECT count(*) FROM entra_groups WHERE workspace_id IS NULL;`
|
||||
- `SELECT count(*) FROM findings WHERE workspace_id IS NULL;`
|
||||
- `SELECT count(*) FROM entra_role_definitions WHERE workspace_id IS NULL;`
|
||||
- `SELECT count(*) FROM tenant_permissions WHERE workspace_id IS NULL;`
|
||||
|
||||
Audit invariant:
|
||||
- `SELECT count(*) FROM audit_logs WHERE tenant_id IS NOT NULL AND workspace_id IS NULL;`
|
||||
|
||||
## Rollback notes
|
||||
|
||||
- Phase 1 migrations are reversible (drop columns / indexes) but may be large operations on production datasets.
|
||||
- Prefer forward-fix for production if Phase 2/3 is partially applied.
|
||||
|
||||
84
specs/093-scope-001-workspace-id-isolation/research.md
Normal file
84
specs/093-scope-001-workspace-id-isolation/research.md
Normal file
@ -0,0 +1,84 @@
|
||||
# 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-001’s 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 spec’s 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 constitution’s 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.
|
||||
|
||||
165
specs/093-scope-001-workspace-id-isolation/spec.md
Normal file
165
specs/093-scope-001-workspace-id-isolation/spec.md
Normal file
@ -0,0 +1,165 @@
|
||||
#+#+#+#+markdown
|
||||
# Feature Specification: SCOPE-001 Workspace ID Isolation
|
||||
|
||||
**Feature Branch**: `093-scope-001-workspace-id-isolation`
|
||||
**Created**: 2026-02-14
|
||||
**Status**: Draft
|
||||
**Input**: Enforce workspace isolation by binding all tenant-owned records to a workspace, with a safe staged rollout and corrected audit invariants.
|
||||
|
||||
## Spec Scope Fields *(mandatory)*
|
||||
|
||||
- **Scope**: workspace
|
||||
- **Primary Routes**: No new pages/routes. Affects all create/write paths that persist tenant-owned records in the tables listed below, plus operational tooling for a one-time backfill.
|
||||
- **Data Ownership**:
|
||||
- Tenant-owned tables impacted (must become explicitly workspace-bound):
|
||||
- policies
|
||||
- policy_versions
|
||||
- backup_sets
|
||||
- backup_items
|
||||
- restore_runs
|
||||
- backup_schedules
|
||||
- inventory_items
|
||||
- inventory_links
|
||||
- entra_groups
|
||||
- findings
|
||||
- entra_role_definitions
|
||||
- tenant_permissions
|
||||
- Audit trail invariants impacted: audit_logs
|
||||
- **RBAC**: No user-facing permissions change. Backfill execution is an operator-only workflow (platform/ops).
|
||||
|
||||
## Clarifications
|
||||
|
||||
### Session 2026-02-14
|
||||
|
||||
- Q: For the 12 tenant-owned tables, how strict should deterministic workspace binding be when a caller explicitly provides a workspace binding? → A: Reject any mismatch; if a record references a tenant, its workspace binding MUST equal the tenant’s workspace.
|
||||
- Q: During backfill, what should happen if a row’s tenant reference is invalid (tenant missing / cannot map tenant → workspace)? → A: Abort the backfill run and report the offending table plus sample identifiers for remediation.
|
||||
- Q: Should tenant_id be allowed to change on existing rows in the 12 tenant-owned tables? → A: No; tenant_id is immutable and updates are rejected.
|
||||
- Q: How should the backfill workflow be recorded for observability/audit? → A: Create/reuse an OperationRun for the backfill and also write an AuditLog summary for start/end/outcome.
|
||||
- Q: Should this feature include updating existing canonical/operational queries/views to scope by workspace binding? → A: No; query/view changes are out of scope for 093 (follow-up later).
|
||||
|
||||
## User Scenarios & Testing *(mandatory)*
|
||||
|
||||
### User Story 1 - Enforce workspace ownership at the data layer (Priority: P1)
|
||||
|
||||
As a platform owner, I want every tenant-owned record to be explicitly bound to a workspace so that workspace isolation does not depend on application-only guardrails.
|
||||
|
||||
**Why this priority**: This closes a class of potential cross-workspace data leakage and makes operational reporting safer and simpler.
|
||||
|
||||
**Independent Test**: Can be tested by creating a tenant-owned record with a tenant reference and asserting that `workspace_id` is derived from the tenant and that any explicit mismatched `workspace_id` is rejected; backfill behavior is validated in User Story 2.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** tenant-owned records exist without a workspace binding, **When** the staged rollout completes, **Then** all tenant-owned records are workspace-bound.
|
||||
2. **Given** the rollout is in progress, **When** the system is used normally, **Then** there is no required downtime for administrators.
|
||||
|
||||
---
|
||||
|
||||
### User Story 2 - Safely backfill existing production data (Priority: P2)
|
||||
|
||||
As an operator, I want a safe, resumable way to backfill missing workspace bindings so that large datasets can be migrated without risky one-shot operations.
|
||||
|
||||
**Why this priority**: Without a safe backfill, enforcing data-level constraints risks outages and operational incidents.
|
||||
|
||||
**Independent Test**: Can be tested by seeding rows with missing workspace bindings, running the backfill workflow twice, and confirming idempotent outcomes.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** a table contains rows missing workspace bindings, **When** the backfill is executed, **Then** those rows are updated to the correct workspace.
|
||||
2. **Given** the backfill is interrupted and restarted, **When** it runs again, **Then** it resumes safely without corrupting already-correct rows.
|
||||
|
||||
---
|
||||
|
||||
### User Story 3 - Make audit logs unambiguous across scopes (Priority: P3)
|
||||
|
||||
As an auditor, I want tenant-scoped audit events to always be workspace-bound so that workspace isolation semantics are preserved in audit trails.
|
||||
|
||||
**Why this priority**: Audit trails are only reliable if their scope is structurally consistent and queryable.
|
||||
|
||||
**Independent Test**: Can be tested by attempting to store a tenant-scoped audit entry without a workspace binding and verifying it is rejected, while allowing workspace-only and platform-only entries.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** an audit log entry references a tenant, **When** it is persisted, **Then** it must also reference a workspace.
|
||||
2. **Given** an audit log entry is workspace-only or platform-only, **When** it is persisted, **Then** it remains valid according to the documented rules.
|
||||
|
||||
---
|
||||
|
||||
### Edge Cases
|
||||
|
||||
- Tenant-owned row references a tenant that no longer exists (or is otherwise invalid).
|
||||
- Backfill is executed concurrently (must not produce conflicting outcomes).
|
||||
- New rows are created while the backfill is in progress (must not reintroduce missing bindings).
|
||||
- Partial completion: some tables are fully backfilled while others are pending.
|
||||
- Mixed-scope audit events: tenant-scoped vs workspace-only vs platform-only.
|
||||
- Attempt to change tenant_id on an existing tenant-owned record.
|
||||
|
||||
## Requirements *(mandatory)*
|
||||
|
||||
**Constitution alignment (required):** If this feature introduces any Microsoft Graph calls, any write/change behavior,
|
||||
or any long-running/queued/scheduled work, the spec MUST describe contract registry updates, safety gates
|
||||
(preview/confirmation/audit), tenant isolation, run observability (`OperationRun` type/identity/visibility), and tests.
|
||||
If security-relevant DB-only actions intentionally skip `OperationRun`, the spec MUST describe `AuditLog` entries.
|
||||
|
||||
**Constitution alignment (RBAC-UX):** If this feature introduces or changes authorization behavior, the spec MUST:
|
||||
- state which authorization plane(s) are involved (tenant/admin `/admin` + tenant-context `/admin/t/{tenant}/...` vs platform `/system`),
|
||||
- ensure any cross-plane access is deny-as-not-found (404),
|
||||
- explicitly define 404 vs 403 semantics:
|
||||
- non-member / not entitled to workspace scope OR tenant scope → 404 (deny-as-not-found)
|
||||
- member but missing capability → 403
|
||||
- describe how authorization is enforced server-side (Gates/Policies) for every mutation/operation-start/credential change,
|
||||
- reference the canonical capability registry (no raw capability strings; no role-string checks in feature code),
|
||||
- ensure global search is tenant-scoped and non-member-safe (no hints; inaccessible results treated as 404 semantics),
|
||||
- ensure destructive-like actions require confirmation (`->requiresConfirmation()`),
|
||||
- include at least one positive and one negative authorization test, and note any RBAC regression tests added/updated.
|
||||
|
||||
**Constitution alignment (OPS-EX-AUTH-001):** OIDC/SAML login handshakes may perform synchronous outbound HTTP (e.g., token exchange)
|
||||
on `/auth/*` endpoints without an `OperationRun`. This MUST NOT be used for Monitoring/Operations pages.
|
||||
|
||||
**Constitution alignment (BADGE-001):** If this feature changes status-like badges (status/outcome/severity/risk/availability/boolean),
|
||||
the spec MUST describe how badge semantics stay centralized (no ad-hoc mappings) and which tests cover any new/changed values.
|
||||
|
||||
**Constitution alignment (Filament Action Surfaces):** If this feature adds or modifies any Filament Resource / RelationManager / Page,
|
||||
the spec MUST include a “UI Action Matrix” (see below) and explicitly state whether the Action Surface Contract is satisfied.
|
||||
If the contract is not satisfied, the spec MUST include an explicit exemption with rationale.
|
||||
|
||||
### Functional Requirements
|
||||
|
||||
- **FR-001 (Schema coverage)**: The system MUST represent a workspace binding for every tenant-owned record in the 12 listed tables.
|
||||
- **FR-002 (No missing bindings post-rollout)**: After completion of the rollout, the system MUST prevent creation or persistence of tenant-owned records without a workspace binding.
|
||||
- **FR-003 (Deterministic binding for new writes)**: For any new or updated tenant-owned record, the system MUST derive the workspace binding from the referenced tenant (not from user/session context alone).
|
||||
- **FR-003a (Mismatch handling)**: If a caller provides a workspace binding that does not match the referenced tenant’s workspace, the write MUST be rejected.
|
||||
- **FR-004 (Safe staged rollout)**: The system MUST support a staged rollout that allows introducing the new field, deploying write-path enforcement, backfilling existing data, and only then enforcing strict constraints.
|
||||
- **FR-005 (Idempotent backfill)**: Operators MUST be able to re-run the backfill safely; repeated runs MUST not regress already-correct data.
|
||||
- **FR-006 (Operational safety)**: The backfill workflow MUST be safe for large datasets (batching/resume) and MUST prevent concurrent executions.
|
||||
- **FR-006a (Invalid mapping handling)**: If the backfill workflow encounters a tenant-owned row that cannot be mapped from tenant → workspace, it MUST abort and report the offending table and sample identifiers for operator remediation.
|
||||
- **FR-007 (Validation)**: Operators MUST be able to validate that no tenant-owned records remain without a workspace binding before strict constraints are enforced.
|
||||
- **FR-011 (Tenant immutability)**: For tenant-owned records, tenant identity MUST be immutable after creation; attempts to change tenant_id MUST be rejected.
|
||||
- **FR-012 (Backfill observability and audit)**: The backfill workflow MUST be observable via an OperationRun (progress + outcome) and MUST write an audit log summary entry for start/end/outcome.
|
||||
- **FR-013 (Query/view scope)**: This feature MUST NOT require broad refactors of canonical/operational queries; it MUST focus on making workspace scoping structurally correct at the data layer.
|
||||
- **FR-008 (Audit log invariant)**: If an audit log entry references a tenant, it MUST also reference a workspace.
|
||||
- **FR-009 (Audit scope flexibility)**: The audit log MUST continue to support workspace-only events (workspace present, tenant absent) and platform-only events (both absent).
|
||||
- **FR-010 (Canonical view scoping readiness)**: After rollout, operational/canonical queries SHOULD be able to scope tenant-owned data by workspace without relying on implicit joins or assumptions.
|
||||
|
||||
### Assumptions & Dependencies
|
||||
|
||||
- Each tenant belongs to exactly one workspace, and that mapping is the source of truth for deriving workspace bindings.
|
||||
- The 12 listed tables are “tenant-owned” per SCOPE-001 and are expected to remain tenant-owned.
|
||||
- Any existing tooling that creates tenant-owned records has enough information to reference the tenant (directly or indirectly) at write time.
|
||||
|
||||
### Key Entities *(include if feature involves data)*
|
||||
|
||||
- **Workspace**: The top-level isolation boundary for data access and operations.
|
||||
- **Tenant**: A unit of configuration/data that is owned by exactly one workspace.
|
||||
- **Tenant-owned record**: Any record that must be both tenant-scoped and workspace-scoped.
|
||||
- **Audit event**: An immutable entry describing an action/event, which may be tenant-scoped, workspace-scoped, or platform-scoped.
|
||||
- **Backfill run**: A controlled operational execution that updates legacy data and reports progress/outcomes.
|
||||
|
||||
## Success Criteria *(mandatory)*
|
||||
|
||||
### Measurable Outcomes
|
||||
|
||||
- **SC-001 (Completeness)**: 100% of records in the 12 tenant-owned tables have a workspace binding after backfill and validation.
|
||||
- **SC-002 (Integrity)**: After strict constraints are enabled, creating a tenant-owned record without a workspace binding is rejected.
|
||||
- **SC-003 (Audit correctness)**: 100% of tenant-scoped audit events include a workspace binding; workspace-only and platform-only audit events remain valid.
|
||||
- **SC-004 (Operational safety)**: The rollout requires no planned downtime for administrators.
|
||||
- **SC-005 (Repeatability)**: Re-running the backfill after completion does not change already-correct records and can be used as a safety check.
|
||||
210
specs/093-scope-001-workspace-id-isolation/tasks.md
Normal file
210
specs/093-scope-001-workspace-id-isolation/tasks.md
Normal file
@ -0,0 +1,210 @@
|
||||
# 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 (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)
|
||||
Loading…
Reference in New Issue
Block a user