TenantAtlas/specs/109-review-pack-export/tasks.md

22 KiB
Raw Blame History

Tasks: Tenant Review Pack Export v1 (CSV + ZIP)

Input: Design documents from /specs/109-review-pack-export/ Prerequisites: plan.md, spec.md, research.md, data-model.md, contracts/api-contracts.md, quickstart.md

Tests: Required (Pest Feature tests). Seven test files covering generation, download, RBAC, prune, resource, widget, and schedule. Operations: OperationRun of type tenant.review_pack.generate tracks all generation runs. Failure reason_code stored in context jsonb column. Active-run dedupe via application-level guard (scopeActive()). RBAC:

  • Gate/Policy enforcement: REVIEW_PACK_VIEW (list + download), REVIEW_PACK_MANAGE (generate + expire).
  • Non-member → 404 (deny-as-not-found, workspace/tenant isolation). Member missing capability → 403.
  • Capabilities registered in canonical Capabilities.php registry — no raw strings in feature code.
  • Authorization plane: tenant-context (/admin/t/{tenant}/...).
  • ReviewPackResource does NOT participate in global search (intentionally excluded; no $recordTitleAttribute).
  • Destructive actions (Expire) use ->requiresConfirmation().
  • Tests include positive + negative authorization scenarios. Filament UI Action Surfaces: UI Action Matrix in spec.md fulfilled. List: header "Generate Pack" modal, row "Download" + "Expire" (max 2 visible), empty state CTA. View: header "Download" + "Regenerate". Dashboard card: "Generate" + "Download" conditional. Filament UI UX-001: View page uses Infolist (not disabled edit form). All modal fields inside Sections. Status badges use BADGE-001. Empty state has title + explanation + 1 CTA. Clickable rows via recordUrl(). Badges: ReviewPackStatus domain added to BadgeDomain enum with ReviewPackStatusBadge mapper per BADGE-001. Tests assert each status renders correct badge.

Organization: Tasks grouped by user story per spec.md priorities (P1 → P2 → P3).

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

  • [P]: Can run in parallel (different files, no dependencies on incomplete tasks)
  • [Story]: Which user story (US1US5) this task belongs to
  • Include exact file paths in descriptions

Phase 1: Setup

Purpose: Database schema — must complete before model/factory creation

  • T001 Create migration for review_packs table with all columns (id, workspace_id FK, tenant_id FK, operation_run_id FK nullable, initiated_by_user_id FK nullable, status, fingerprint, previous_fingerprint, summary jsonb, options jsonb, file_disk, file_path, file_size bigint, sha256, generated_at timestampTz, expires_at timestampTz, timestamps), three indexes, and partial unique index WHERE fingerprint IS NOT NULL AND status NOT IN ('expired','failed') in database/migrations/XXXX_create_review_packs_table.php

Phase 2: Foundational (Blocking Prerequisites)

Purpose: Core enums, config, capabilities, badges, model, and factory that ALL user stories depend on

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

  • T002 [P] Create ReviewPackStatus enum with 5 string-backed cases (Queued, Generating, Ready, Failed, Expired) in app/Support/ReviewPackStatus.php
  • T003 [P] Add ReviewPackGenerate case with value tenant.review_pack.generate to app/Support/OperationRunType.php
  • T004 [P] Add REVIEW_PACK_VIEW = 'review_pack.view' and REVIEW_PACK_MANAGE = 'review_pack.manage' constants to app/Support/Auth/Capabilities.php
  • T005 [P] Add ReviewPackStatus = 'review_pack_status' case to app/Support/Badges/BadgeDomain.php
  • T006 [P] Create ReviewPackStatusBadge mapper returning BadgeSpec for 5 statuses (queued→warning, generating→info, ready→success, failed→danger, expired→gray) in app/Support/Badges/Mappers/ReviewPackStatusBadge.php
  • T007 [P] Add exports disk with local driver at storage_path('app/private/exports'), serve => false, throw => true to config/filesystems.php
  • T008 [P] Add review_pack config section with retention_days (90), hard_delete_grace_days (30), download_url_ttl_minutes (60), include_pii_default (true), include_operations_default (true) to config/tenantpilot.php
  • T009 Create ReviewPack model with DerivesWorkspaceIdFromTenant + HasFactory traits, guarded = [], casts (summary/options→array, generated_at/expires_at→datetime, file_size→integer), relationships (workspace, tenant, operationRun, initiator via initiated_by_user_id), scopes (ready, expired, pastRetention, forTenant, latestReady), and STATUS_* constants in app/Models/ReviewPack.php
  • T010 Create ReviewPackFactory with default definition (ready state) and 5 named states (queued, generating, ready, failed, expired) that set correct status + nullable file fields per state in database/factories/ReviewPackFactory.php
  • T011 Run migration and verify model + factory (vendor/bin/sail artisan migrate && vendor/bin/sail artisan test --compact --filter=ReviewPack)

Checkpoint: Foundation ready — user story implementation can now begin


Phase 3: User Story 1 — Generate and Download Review Pack (Priority: P1) 🎯 MVP

Goal: Produce an auditable ZIP artifact (summary.json, findings.csv, operations.csv, hardening.json, reports/*.json, metadata.json) from cached DB data, store privately on exports disk, deliver via signed URL, and provide full Filament UI.

Independent Test: Create tenant with stored reports + findings, trigger generation as REVIEW_PACK_MANAGE user, assert OperationRun created, review_packs row transitions to ready, ZIP file exists on exports disk with correct 7-file set, download via signed URL returns correct headers and content.

Implementation for User Story 1

  • T012 [US1] Implement ReviewPackService with generate(Tenant, User, array $options): ReviewPack (creates OperationRun + ReviewPack + dispatches job), computeFingerprint(Tenant, array $options): string (SHA-256 of tenant_id + options + report fingerprints + max finding last_seen_at + hardening tuple), generateDownloadUrl(ReviewPack): string (URL::signedRoute with configurable TTL), findExistingPack(Tenant, string $fingerprint): ?ReviewPack (ready + unexpired dedupe check), and checkActiveRun(Tenant): bool (active OperationRun guard) in app/Services/ReviewPackService.php
  • T013 [US1] Implement GenerateReviewPackJob (ShouldQueue) with 12-step pipeline: load records, mark running, collect StoredReports (permission_posture + entra.admin_roles), collect Findings (status in open/acknowledged, chunked), collect tenant hardening fields (via accessor, not raw DB), collect recent OperationRuns (30 days), compute data_freshness, build file map with PII redaction when include_pii=false, assemble ZIP via ZipArchive (alphabetical order, temp file), compute SHA-256, store on exports disk, update ReviewPack (status=ready, fingerprint, sha256, file_size, file_path, file_disk, generated_at, summary, expires_at), mark OperationRun completed; on failure: mark failed with reason_code in context, notify initiator in app/Jobs/GenerateReviewPackJob.php
  • T014 [P] [US1] Create ReviewPackStatusNotification (database channel) with conditional rendering: ready payload (title, body with tenant name, View action URL) and failed payload (title, body with sanitized reason, View action URL) in app/Notifications/ReviewPackStatusNotification.php
  • T015 [US1] Create ReviewPackDownloadController with single __invoke(Request, ReviewPack) method: validate status is ready, stream file via Storage::disk($pack->file_disk)->download() with headers Content-Type application/zip, Content-Disposition attachment with filename review-pack-{tenant_external_id}-{YYYY-MM-DD}.zip, X-Review-Pack-SHA256; return 404 for expired/non-ready packs in app/Http/Controllers/ReviewPackDownloadController.php
  • T016 [US1] Add signed download route GET /admin/review-packs/{reviewPack}/download named admin.review-packs.download with signed middleware in routes/web.php
  • T017 [US1] Create ReviewPackResource with: table columns (status badge via BADGE-001, tenant name, generated_at datetime, expires_at datetime, file_size formatted), recordUrl() for clickable rows to ViewReviewPack, header action "Generate Pack" (modal with Section containing include_pii Toggle + include_operations Toggle, authorized by REVIEW_PACK_MANAGE, calls ReviewPackService::generate), row actions "Download" (visible when ready, REVIEW_PACK_VIEW, openUrlInNewTab with signed URL) + "Expire" (REVIEW_PACK_MANAGE, color danger, requiresConfirmation, sets status=expired + deletes file), empty state with title "No review packs yet" + description + "Generate first pack" CTA, filters (status SelectFilter, generated_at date range), search on tenant name, sort on generated_at/status, nav group Monitoring → Exports in app/Filament/Resources/ReviewPackResource.php
  • T018 [US1] Create ListReviewPacks page extending ListRecords in app/Filament/Resources/ReviewPackResource/Pages/ListReviewPacks.php
  • T019 [US1] Create ViewReviewPack page with Infolist layout: status badge (BADGE-001), summary section (data_freshness per-source timestamps, generated_at, expires_at, file_size, sha256), options section (include_pii, include_operations), initiator + OperationRun link; header actions "Download" (REVIEW_PACK_VIEW, visible when ready) + "Regenerate" (REVIEW_PACK_MANAGE, requiresConfirmation when ready pack exists, pre-fills current options, sets previous_fingerprint) in app/Filament/Resources/ReviewPackResource/Pages/ViewReviewPack.php
  • T020 [P] [US1] Create TenantReviewPackCard widget with 5 display states: no pack ("No review pack yet" + Generate CTA), queued/generating (status badge + "Generation in progress"), ready (badge + generated_at + expires_at + file_size + Download action REVIEW_PACK_VIEW + "Generate new" REVIEW_PACK_MANAGE), failed (badge + sanitized reason + Retry=Generate REVIEW_PACK_MANAGE), expired (badge + expiry date + "Generate new" REVIEW_PACK_MANAGE); data = latest ReviewPack for current tenant in app/Filament/Widgets/TenantReviewPackCard.php
  • T021 [P] [US1] Create generation tests: happy path (job processes, status→ready, file on disk, sha256+file_size populated, notification sent to initiator, OperationRun completed+success), failure path (exception → status→failed, OperationRun failed, reason_code in context, failure notification), empty reports (job succeeds with empty report sections, status→ready), PII redaction (include_pii=false → display_name replaced with placeholder, UUIDs retained), ZIP contents (exactly 7 files in alphabetical order: findings.csv, hardening.json, metadata.json, operations.csv, reports/entra_admin_roles.json, reports/permission_posture.json, summary.json), performance baseline (seed 1,000 findings + 10 stored reports and assert job completes within 60s per SC-001) in tests/Feature/ReviewPack/ReviewPackGenerationTest.php
  • T022 [P] [US1] Create download tests: signed URL → 200 with Content-Type application/zip + Content-Disposition + X-Review-Pack-SHA256 headers; expired signature → 403; expired pack (status=expired) → 404; non-ready pack (status=queued) → 404; non-existent pack → 404 in tests/Feature/ReviewPack/ReviewPackDownloadTest.php
  • T023 [P] [US1] Create resource Livewire tests: list page renders with columns, empty state shows CTA, generate action modal opens with toggle fields, view page displays infolist sections with correct data, expire action requires confirmation and updates status in tests/Feature/ReviewPack/ReviewPackResourceTest.php
  • T024 [P] [US1] Create widget Livewire tests: no-pack state shows generate CTA, ready state shows download + generate actions, generating state shows in-progress message, failed state shows retry action, expired state shows generate action in tests/Feature/ReviewPack/ReviewPackWidgetTest.php

Checkpoint: User Story 1 fully functional — can generate, download, view, and manage review packs


Phase 4: User Story 2 — Fingerprint Dedupe (Priority: P2)

Goal: Prevent duplicate pack generation; reuse existing ready pack with same fingerprint; reject concurrent generation for same tenant.

Independent Test: Trigger generation twice with identical inputs, assert only one ready ReviewPack row and one file on disk.

Implementation for User Story 2

  • T025 [US2] Add fingerprint dedupe integration tests: identical inputs → returns existing ready pack (no new row, no new file), active OperationRun for same tenant → rejection with "generation already in progress" notification, expired pack with same fingerprint → allows new generation (partial unique index excludes expired), different options (include_pii toggled) → new pack with different fingerprint, fingerprint computation is deterministic (same inputs → same hash) in tests/Feature/ReviewPack/ReviewPackGenerationTest.php

Checkpoint: Fingerprint dedupe verified — duplicate requests handled correctly


Phase 5: User Story 3 — RBAC Enforcement (Priority: P2)

Goal: Non-members get 404 (deny-as-not-found), view-only members can list + download but not generate, manage members can generate + expire.

Independent Test: Test each role (no membership, REVIEW_PACK_VIEW, REVIEW_PACK_MANAGE) against each action (list, view, download, generate, expire) and assert correct HTTP status or Filament authorization result.

Implementation for User Story 3

  • T026 [US3] Create ReviewPackPolicy with viewAny, view, create, delete methods mapping to REVIEW_PACK_VIEW (viewAny, view) and REVIEW_PACK_MANAGE (create, delete) capability checks via canonical registry; enforce workspace_id + tenant_id scope (non-member → null = 404 semantics) in app/Policies/ReviewPackPolicy.php
  • T027 [US3] Create comprehensive RBAC enforcement tests: non-member → 404 on list page, non-member → 404 on view page, non-member → 404 on download route; REVIEW_PACK_VIEW member → list succeeds, view succeeds, download signed URL succeeds, generate action hidden/403; REVIEW_PACK_MANAGE member → generate succeeds, expire succeeds, download succeeds; signed URL without valid signature → 403; expire action shows requiresConfirmation dialog before execution in tests/Feature/ReviewPack/ReviewPackRbacTest.php

Checkpoint: RBAC enforcement verified across all surfaces (resource, controller, widget)


Phase 6: User Story 4 — Retention & Prune (Priority: P3)

Goal: Auto-expire packs past retention deadline, delete storage files, optionally hard-delete DB rows after grace period. Clean up dead sla_due AlertRule option.

Independent Test: Create ReviewPack with expires_at in past, run prune command, verify status=expired and file deleted from exports disk.

Implementation for User Story 4

  • T028 [US4] Create PruneReviewPacksCommand with signature tenantpilot:review-pack:prune {--hard-delete}: query ready packs where expires_at < now → set status=expired + delete file from exports disk; when --hard-delete: query expired packs where updated_at < now - grace_days → hard-delete rows; output summary "{n} packs expired, {m} packs hard-deleted" in app/Console/Commands/PruneReviewPacksCommand.php
  • T029 [US4] Add prune command schedule entry tenantpilot:review-pack:prune with daily() + withoutOverlapping() in routes/console.php
  • T030 [P] [US4] Remove or hide sla_due event_type option from AlertRuleResource form dropdown (keep EVENT_SLA_DUE constant on model for backward compatibility) in app/Filament/Resources/AlertRuleResource.php
  • T031 [US4] Create prune tests: past retention → status=expired + file deleted from disk; future retention → unaffected; --hard-delete past grace_days → rows removed from DB; --hard-delete within grace_days → rows kept; command output includes correct counts; AlertRule form no longer shows sla_due option in dropdown in tests/Feature/ReviewPack/ReviewPackPruneTest.php

Checkpoint: Retention automation verified — packs expire and files cleaned up automatically on schedule


Phase 7: User Story 5 — Scheduled Scan Wiring (Priority: P3)

Goal: Ensure daily permission-posture and Entra admin-roles scans are wired in the scheduler so review packs consume fresh cached data.

Independent Test: Assert the Laravel console schedule contains the expected dispatch entries with at least daily frequency.

Implementation for User Story 5

  • T032 [US5] Verify existing Entra admin roles schedule closure in routes/console.php runs daily; add TODO comment for tenantpilot:posture:dispatch command (creation deferred per research.md — command infrastructure does not yet exist; FR-015 partially fulfilled)
  • T033 [US5] Create schedule assertion test: Entra admin roles dispatch appears with daily frequency; document posture:dispatch deferral as explicit test skip with rationale in tests/Feature/ReviewPack/ReviewPackScheduleTest.php

Checkpoint: Scheduled scan wiring verified or documented as deferred


Phase 8: Polish & Cross-Cutting Concerns

Purpose: Code quality, formatting, and end-to-end validation

  • T034 [P] Run Pint formatter on all new and modified files (vendor/bin/sail bin pint --dirty --format agent)
  • T035 Run full ReviewPack test suite and fix any failures (vendor/bin/sail artisan test --compact tests/Feature/ReviewPack/)
  • T036 Validate quickstart.md scenarios end-to-end: create tenant with stored reports + findings, generate pack, verify ZIP contents (7 files), download via signed URL, expire pack, run prune command, confirm file deleted

Dependencies & Execution Order

Phase Dependencies

  • Setup (Phase 1): No dependencies — can start immediately
  • Foundational (Phase 2): Depends on Phase 1 (migration must exist) — BLOCKS all user stories
  • US1 (Phase 3): Depends on Phase 2 — delivers complete MVP
  • US2 (Phase 4): Depends on Phase 3 (needs generate pipeline to test dedupe)
  • US3 (Phase 5): Depends on Phase 3 (needs resource + controller to test RBAC)
  • US4 (Phase 6): Depends on Phase 2 only (prune operates on model, not UI) — can parallel with US1
  • US5 (Phase 7): Depends on Phase 2 only — can parallel with US1
  • Polish (Phase 8): Depends on all desired phases being complete

User Story Dependencies

  • US1 (P1): Core MVP — blocked only by Foundational phase
  • US2 (P2): Tests dedupe behavior in generate pipeline — depends on US1
  • US3 (P2): Tests RBAC across all surfaces — depends on US1
  • US4 (P3): Prune command + schedule — can start after Foundational (parallel with US1 if desired)
  • US5 (P3): Schedule wiring — can start after Foundational (parallel with US1 if desired)

Within Each User Story

  • Service (T012) before Job (T013) — job calls service methods
  • Controller (T015) + Route (T016) before Resource download action (T017)
  • Resource (T017) before Pages (T018, T019)
  • All implementations before their corresponding tests
  • Service (T012) before Widget (T020)

Parallel Opportunities

  • Phase 2: All tasks T002T008 run in parallel (7 independent files)
  • Phase 3: T014 (notification) parallels T013 (job); T020 (widget) parallels T017T019 (resource/pages) after T012 (service) completes
  • Phase 3 tests: T021T024 all run in parallel (4 independent test files)
  • Cross-phase: US4 (T028T031) can parallel with US1 (T012T024) if team capacity allows
  • Cross-phase: US5 (T032T033) can parallel with US1

Parallel Example: Foundational Phase

# Launch all parallel foundational tasks together:
T002: ReviewPackStatus enum       → app/Support/ReviewPackStatus.php
T003: OperationRunType case       → app/Support/OperationRunType.php
T004: Capabilities constants      → app/Support/Auth/Capabilities.php
T005: BadgeDomain case            → app/Support/Badges/BadgeDomain.php
T006: ReviewPackStatusBadge       → app/Support/Badges/Mappers/ReviewPackStatusBadge.php
T007: exports disk config         → config/filesystems.php
T008: review_pack config          → config/tenantpilot.php

# Then sequentially (dependency chain):
T009: ReviewPack model            → app/Models/ReviewPack.php (needs T002)
T010: ReviewPackFactory           → database/factories/ReviewPackFactory.php (needs T009)
T011: Migrate + verify

Parallel Example: US1 Tests

# After all US1 implementations complete, launch test files in parallel:
T021: ReviewPackGenerationTest    → tests/Feature/ReviewPack/ReviewPackGenerationTest.php
T022: ReviewPackDownloadTest      → tests/Feature/ReviewPack/ReviewPackDownloadTest.php
T023: ReviewPackResourceTest      → tests/Feature/ReviewPack/ReviewPackResourceTest.php
T024: ReviewPackWidgetTest        → tests/Feature/ReviewPack/ReviewPackWidgetTest.php

Implementation Strategy

MVP First (User Story 1 Only)

  1. Complete Phase 1: Setup (migration)
  2. Complete Phase 2: Foundational (model, enums, config, badge)
  3. Complete Phase 3: User Story 1 (service, job, controller, resource, widget, tests)
  4. STOP and VALIDATE: Run vendor/bin/sail artisan test --compact tests/Feature/ReviewPack/
  5. Deploy/demo if ready — full generate + download + UI works

Incremental Delivery

  1. Setup + Foundational → Foundation ready
  2. Add US1 → Generate + Download + UI works → Deploy MVP
  3. Add US2 → Dedupe verified → Add US3 → RBAC hardened → Deploy
  4. Add US4 → Retention automation → Add US5 → Schedule wiring → Deploy
  5. Polish → Final validation → Production-ready

Parallel Team Strategy

With multiple developers:

  1. Team completes Setup + Foundational together
  2. Once Foundational is done:
    • Developer A: US1 (core pipeline + UI)
    • Developer B: US4 (prune command — independent of UI)
  3. After US1 complete:
    • Developer A: US2 + US3 (dedupe + RBAC tests)
    • Developer B: US5 (schedule wiring)
  4. Polish phase together

Notes

  • [P] tasks = different files, no dependencies on incomplete tasks
  • [Story] label maps task to specific user story for traceability
  • Tests are REQUIRED per project conventions (Pest Feature tests)
  • FR-015 (tenantpilot:posture:dispatch) partially deferred — command infrastructure does not yet exist (documented in research.md §7)
  • FR-016 (sla_due cleanup) is a minor AlertRule form change in US4 phase
  • ReviewPackResource does NOT participate in global search (intentional, per plan.md Filament v5 contract §3)
  • All destructive actions (Expire) use ->requiresConfirmation() per Filament v5 blueprint §9
  • No custom frontend assets — standard Filament components only
  • Commit after each task or logical group; stop at any checkpoint to validate independently