132 lines
8.5 KiB
Markdown
132 lines
8.5 KiB
Markdown
# Implementation Plan: Tenant Review Pack Export v1 (CSV + ZIP)
|
|
|
|
**Branch**: `109-review-pack-export` | **Date**: 2026-02-23 | **Spec**: [spec.md](spec.md)
|
|
**Input**: Feature specification from `/specs/109-review-pack-export/spec.md`
|
|
|
|
## Summary
|
|
|
|
Implement an exportable audit artifact (ZIP with CSV + JSON) for MSP/Enterprise tenant reviews. The system collects pre-cached stored reports, findings, tenant hardening status, and recent operations from the database (no Graph API calls), assembles them into a deterministic ZIP archive, and stores it on a private local disk. Downloads use signed temporary URLs. Features include fingerprint-based content deduplication, RBAC-gated access (`REVIEW_PACK_VIEW` / `REVIEW_PACK_MANAGE`), retention pruning with configurable expiry, and OperationRun-based observability. The Filament UI provides a ReviewPackResource (list + view), a Tenant Dashboard card, and a modal-based generation trigger.
|
|
|
|
## Technical Context
|
|
|
|
**Language/Version**: PHP 8.4 (Laravel 12)
|
|
**Primary Dependencies**: Filament v5, Livewire v4, Laravel Framework v12
|
|
**Storage**: PostgreSQL (jsonb columns for summary/options), local filesystem (`exports` disk) for ZIP artifacts
|
|
**Testing**: Pest v4 (Feature tests with Livewire component testing)
|
|
**Target Platform**: Linux server (Sail/Docker local, Dokploy VPS staging/production)
|
|
**Project Type**: web (Laravel monolith with Filament admin panel)
|
|
**Performance Goals**: Pack generation < 60s for 1,000 findings + 10 stored reports; download streaming with no memory loading of full file
|
|
**Constraints**: DB-only generation (no Graph API calls); chunked iteration for large finding sets; temp file for ZIP assembly (no full in-memory ZIP)
|
|
**Scale/Scope**: Multi-workspace, multi-tenant; packs are tenant-scoped; typical pack size < 10 MB
|
|
|
|
## Constitution Check
|
|
|
|
*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
|
|
|
|
| Rule | Status | Notes |
|
|
|------|--------|-------|
|
|
| **Inventory-first** | PASS | ReviewPack reads from existing stored_reports + findings (pre-cached inventory data). No new inventory sources introduced. |
|
|
| **Read/write separation** | PASS | Generation is a queued job (async write). User action is enqueue-only. Destructive actions (expire/delete) require `->requiresConfirmation()`. All writes audit-logged via OperationRun. |
|
|
| **Graph contract path** | PASS | No Graph API calls in this feature. All data sourced from DB. FR-006 explicitly forbids Graph calls during generation. |
|
|
| **Deterministic capabilities** | PASS | `REVIEW_PACK_VIEW` and `REVIEW_PACK_MANAGE` added to canonical `Capabilities.php` registry. Discoverable via `Capabilities::all()` reflection. No raw strings. |
|
|
| **RBAC-UX: two planes** | PASS | All routes under `/admin` plane. Tenant-context routes scoped via `/admin/t/{tenant}/...`. No `/system` plane involvement. |
|
|
| **RBAC-UX: non-member = 404** | PASS | Non-member workspace/tenant access returns 404 (deny-as-not-found). Policy enforces workspace_id + tenant_id before any data returned. Download endpoint returns 404 for non-existent/inaccessible packs. |
|
|
| **RBAC-UX: member missing cap = 403** | PASS | Members without `REVIEW_PACK_VIEW` get 403 on list/download. Members without `REVIEW_PACK_MANAGE` get 403 on generate/expire. |
|
|
| **RBAC-UX: destructive confirmation** | PASS | Expire and Delete actions use `->requiresConfirmation()` with clear warning text. Regenerate requires confirmation if a ready pack exists. |
|
|
| **RBAC-UX: global search** | N/A | ReviewPackResource does not participate in global search (no `$recordTitleAttribute`). Intentionally excluded. |
|
|
| **Workspace isolation** | PASS | `DerivesWorkspaceIdFromTenant` auto-sets workspace_id. All queries scope by workspace_id + tenant_id. Download controller validates pack ownership before streaming. |
|
|
| **Tenant isolation** | PASS | Partial unique index scoped to (workspace_id, tenant_id, fingerprint). All scopes include tenant_id. Dashboard widget reads only current tenant packs. |
|
|
| **Run observability** | PASS | `OperationRun` of type `tenant.review_pack.generate` tracks lifecycle. Active-run dedupe via partial unique index. Failure reason stored in `context['reason_code']`. |
|
|
| **Automation** | PASS | Prune command uses `withoutOverlapping()`. GenerateReviewPackJob unique via OperationRun active dedupe (existing pattern). |
|
|
| **Data minimization** | PASS | FR-008: no webhooks, tokens, secrets, or raw Graph dumps exported. FR-009: PII redaction via `include_pii` toggle. |
|
|
| **BADGE-001** | PASS | `ReviewPackStatus` added to `BadgeDomain` enum with `ReviewPackStatusBadge` mapper. All 5 statuses mapped. |
|
|
| **Filament Action Surface Contract** | PASS | UI Action Matrix in spec. List: header action (Generate), row actions (Download + Expire), empty state CTA. View: header actions (Download + Regenerate). Card: inline actions. Destructive actions confirmed. |
|
|
| **UX-001** | PASS | View page uses Infolist. Empty state has specific title + explanation + 1 CTA. Status badges use BADGE-001. Generate modal fields in Sections. Table has search/sort/filters. |
|
|
|
|
**Post-design re-evaluation**: All checks PASS. No violations found.
|
|
|
|
## Project Structure
|
|
|
|
### Documentation (this feature)
|
|
|
|
```text
|
|
specs/109-review-pack-export/
|
|
├── plan.md # This file
|
|
├── spec.md # Feature specification
|
|
├── research.md # Phase 0: resolved unknowns
|
|
├── data-model.md # Phase 1: entity design
|
|
├── quickstart.md # Phase 1: implementation guide
|
|
├── contracts/ # Phase 1: API + action contracts
|
|
│ └── api-contracts.md
|
|
├── checklists/
|
|
│ └── requirements.md
|
|
└── tasks.md # Phase 2 output (created by /speckit.tasks)
|
|
```
|
|
|
|
### Source Code (repository root)
|
|
|
|
```text
|
|
app/
|
|
├── Console/Commands/
|
|
│ └── PruneReviewPacksCommand.php
|
|
├── Filament/
|
|
│ ├── Resources/
|
|
│ │ └── ReviewPackResource.php
|
|
│ │ └── Pages/
|
|
│ │ ├── ListReviewPacks.php
|
|
│ │ └── ViewReviewPack.php
|
|
│ └── Widgets/
|
|
│ └── TenantReviewPackCard.php
|
|
├── Http/Controllers/
|
|
│ └── ReviewPackDownloadController.php
|
|
├── Jobs/
|
|
│ └── GenerateReviewPackJob.php
|
|
├── Models/
|
|
│ └── ReviewPack.php
|
|
├── Notifications/
|
|
│ └── ReviewPackStatusNotification.php
|
|
├── Services/
|
|
│ └── ReviewPackService.php
|
|
└── Support/
|
|
├── ReviewPackStatus.php
|
|
└── Badges/Mappers/
|
|
└── ReviewPackStatusBadge.php
|
|
|
|
config/
|
|
├── filesystems.php # Modified: add exports disk
|
|
└── tenantpilot.php # Modified: add review_pack section
|
|
|
|
database/
|
|
├── factories/
|
|
│ └── ReviewPackFactory.php
|
|
└── migrations/
|
|
└── XXXX_create_review_packs_table.php
|
|
|
|
routes/
|
|
├── web.php # Modified: add download route
|
|
└── console.php # Modified: add prune schedule entry
|
|
|
|
tests/Feature/ReviewPack/
|
|
├── ReviewPackGenerationTest.php
|
|
├── ReviewPackDownloadTest.php
|
|
├── ReviewPackRbacTest.php
|
|
├── ReviewPackPruneTest.php
|
|
├── ReviewPackResourceTest.php
|
|
└── ReviewPackWidgetTest.php
|
|
```
|
|
|
|
**Structure Decision**: Standard Laravel monolith structure. All new files follow existing directory conventions. No new base folders introduced.
|
|
|
|
## Complexity Tracking
|
|
|
|
No Constitution violations detected. No complexity justifications needed.
|
|
|
|
## Filament v5 Agent Output Contract
|
|
|
|
1. **Livewire v4.0+ compliance**: Yes. Filament v5 requires Livewire v4 — all components are Livewire v4 compatible.
|
|
2. **Provider registration**: Panel provider already registered in `bootstrap/providers.php`. No new panel required.
|
|
3. **Global search**: ReviewPackResource does NOT participate in global search. No `$recordTitleAttribute` set. Intentional.
|
|
4. **Destructive actions**: Expire action uses `->action(...)` + `->requiresConfirmation()` + `->color('danger')`. Regenerate uses `->requiresConfirmation()` when a ready pack exists. Hard-delete in prune command requires explicit `--hard-delete` flag.
|
|
5. **Asset strategy**: No custom frontend assets. Standard Filament components only. `php artisan filament:assets` already in deploy pipeline.
|
|
6. **Testing plan**: Pest Feature tests covering: generation job (happy + failure paths), download controller (signed URL, expired, RBAC), RBAC enforcement (404/403 matrix), prune command (expire + hard-delete), Filament Resource (Livewire component tests for list/view pages + actions), Dashboard widget (Livewire component test).
|