spec: 109 clarify — 5 decisions (download URL, RBAC expire, stale data, prune grace, storage backend)
This commit is contained in:
parent
67e72012fb
commit
d4cbe7b722
@ -13,7 +13,7 @@ ## Spec Scope Fields *(mandatory)*
|
||||
- **Primary Routes**:
|
||||
- `/admin/t/{tenant}/review-packs` — ReviewPackResource list
|
||||
- `/admin/t/{tenant}/review-packs/{id}` — View detail
|
||||
- Download route (authenticated controller action, private storage)
|
||||
- Download route (signed temporary URL via `URL::signedRoute()`, private storage; URL expires after configurable TTL)
|
||||
- Tenant dashboard card (embedded widget/section)
|
||||
- **Data Ownership**: workspace-owned + tenant-scoped (`review_packs` links `workspace_id` + `tenant_id`); binary files on private `exports` disk.
|
||||
- **RBAC**:
|
||||
@ -153,15 +153,15 @@ ### Functional Requirements
|
||||
- **FR-003**: System MUST implement a `ReviewPackResource` Filament Resource under Monitoring → Exports nav group with list, view, and modal-generate.
|
||||
- **FR-004**: System MUST implement a Tenant Dashboard card showing the latest pack's status, `generated_at`, `expires_at`, and "Generate pack" (manage) / "Download latest" (view/manage) actions.
|
||||
- **FR-005**: System MUST implement `tenant.review_pack.generate` OperationRun with active-run dedupe (unique active run per workspace+tenant+run_type) and content dedupe (fingerprint uniqueness per workspace+tenant).
|
||||
- **FR-006**: System MUST implement a queued Job that collects stored reports (`permission_posture`, `entra.admin_roles`), findings (`drift`, `permission_posture`, `entra_admin_roles`), tenant hardening/write-safety status fields, and recent operation_runs (last 30 days), then assembles and stores a ZIP artifact.
|
||||
- **FR-006**: System MUST implement a queued Job that collects stored reports (`permission_posture`, `entra.admin_roles`), findings (`drift`, `permission_posture`, `entra_admin_roles`), tenant hardening/write-safety status fields, and recent operation_runs (last 30 days), then assembles and stores a ZIP artifact. The job MUST NOT trigger Graph API calls; it operates exclusively on existing DB data. `summary.json` MUST include a `data_freshness` object with per-source timestamps indicating the age of each data input.
|
||||
- **FR-007**: The generated ZIP MUST contain: `summary.json`, `findings.csv`, `operations.csv` (default on, togglable), `hardening.json`, `reports/permission_posture.json`, `reports/entra_admin_roles.json`, `metadata.json`. File order in ZIP MUST be stable and deterministic.
|
||||
- **FR-008**: The export MUST NOT include webhook URLs, email recipient lists, tokens, client secrets, or raw Graph API response dumps.
|
||||
- **FR-009**: When `include_pii = false`, system MUST redact `principal.display_name` (and equivalent PII fields) across all exported files, retaining object IDs and types.
|
||||
- **FR-010**: System MUST compute a deterministic fingerprint: `sha256(tenant_id + include_pii + include_operations + sorted_report_fingerprints + max_finding_last_seen_at + hardening_status_tuple)`; a `ready` unexpired pack with the same fingerprint MUST be reused without creating a new artifact.
|
||||
- **FR-011**: Pack files MUST be written to `Storage::disk('exports')` (private, non-public); the download endpoint MUST enforce `REVIEW_PACK_VIEW` before streaming; non-members receive 404.
|
||||
- **FR-011**: Pack files MUST be written to `Storage::disk('exports')` (private, non-public); downloads MUST use signed temporary URLs (`URL::signedRoute()`) with a configurable TTL; the URL generation endpoint MUST enforce `REVIEW_PACK_VIEW` (non-members receive 404); the signed download controller validates the signature but does not require an active session.
|
||||
- **FR-012**: System MUST compute and persist `sha256` and `file_size` for each generated pack.
|
||||
- **FR-013**: System MUST set `expires_at` on each pack (default: 90 days from `generated_at`; configurable via `config('tenantpilot.review_pack.retention_days')`).
|
||||
- **FR-014**: System MUST provide Artisan command `tenantpilot:review-pack:prune` that marks expired packs as `expired`, deletes their storage files, and optionally hard-deletes rows after a grace period.
|
||||
- **FR-014**: System MUST provide Artisan command `tenantpilot:review-pack:prune` that marks packs past `expires_at` as `expired` and deletes their storage files. Hard-delete of DB rows is off by default; when invoked with `--hard-delete`, rows that have been in `expired` status for longer than the grace period (default: 30 days, configurable via `config('tenantpilot.review_pack.hard_delete_grace_days')`) are permanently removed. Without `--hard-delete`, expired rows remain queryable for audit trails.
|
||||
- **FR-015**: System MUST wire `tenantpilot:posture:dispatch` and `tenantpilot:entra-roles:dispatch` in the Laravel console scheduler with at least daily frequency.
|
||||
- **FR-016**: System MUST remove or hide the `sla_due` event_type option from the AlertRule form field without breaking existing AlertRule data.
|
||||
- **FR-017**: System MUST send a DB notification to the initiator when a pack transitions to `ready` (with download link) or `failed` (with reason).
|
||||
@ -174,7 +174,7 @@ ## UI Action Matrix *(mandatory)*
|
||||
|
||||
| Surface | Location | Header Actions | Inspect Affordance | Row Actions (max 2 visible) | Bulk Actions | Empty-State CTA(s) | View Header Actions | Destructive Confirmation | Audit log? | Notes |
|
||||
|---|---|---|---|---|---|---|---|---|---|---|
|
||||
| ReviewPackResource (List) | app/Filament/Resources/ReviewPackResource.php | "Generate Pack" modal (REVIEW_PACK_MANAGE) | Clickable row to View page | "Download" (ready only, REVIEW_PACK_VIEW), "Expire" (Owner/Manager, destructive) | — | "No review packs yet" + "Generate first pack" CTA | — | Expire + Delete require requiresConfirmation() | Yes (OperationRun) | Generate opens options modal with include_pii toggle |
|
||||
| ReviewPackResource (List) | app/Filament/Resources/ReviewPackResource.php | "Generate Pack" modal (REVIEW_PACK_MANAGE) | Clickable row to View page | "Download" (ready only, REVIEW_PACK_VIEW), "Expire" (REVIEW_PACK_MANAGE, destructive + requiresConfirmation) | — | "No review packs yet" + "Generate first pack" CTA | — | Expire + Delete require requiresConfirmation() | Yes (OperationRun) | Generate opens options modal with include_pii toggle |
|
||||
| ReviewPackResource (View) | app/Filament/Resources/ReviewPackResource/Pages/ViewReviewPack.php | "Download" (REVIEW_PACK_VIEW), "Regenerate" (REVIEW_PACK_MANAGE, Owner/Manager) | — | — | — | — | Download, Regenerate | Regenerate requires requiresConfirmation() if ready pack exists | Yes (OperationRun) | Infolist layout; status badge per BADGE-001 |
|
||||
| Tenant Dashboard Card | app/Filament/Widgets/TenantReviewPackCard.php | — | — | "Generate pack" (REVIEW_PACK_MANAGE), "Download latest" (REVIEW_PACK_VIEW) | — | "No pack yet — Generate first" | — | — | Yes (via OperationRun) | Shows latest pack status, generated_at, expires_at |
|
||||
|
||||
@ -205,10 +205,22 @@ ### Measurable Outcomes
|
||||
|
||||
---
|
||||
|
||||
## Clarifications
|
||||
|
||||
### Session 2026-02-23
|
||||
|
||||
- Q: Download delivery mechanism (signed temporary URL vs session-authenticated stream vs both)? → A: Signed temporary URL (`URL::signedRoute()`, self-contained, expires after configurable TTL). Notification links are self-contained; RBAC check happens at URL generation time.
|
||||
- Q: Expire/Delete authorization — reuse `REVIEW_PACK_MANAGE` or add a separate `REVIEW_PACK_DELETE` capability? → A: Reuse `REVIEW_PACK_MANAGE`; the `->requiresConfirmation()` dialog provides the safety gate. A dedicated delete capability can be split out later if needed.
|
||||
- Q: Stale data threshold — should the generation job trigger on-demand Graph scans if stored reports are older than 24h? → A: No. Generate with existing data (DB-only, no Graph calls). Include `data_freshness` timestamps per source in `summary.json` so the reviewer can judge staleness and optionally trigger manual scans before regenerating.
|
||||
- Q: Prune grace period for hard-delete — how long do expired rows stay in the DB, and is hard-delete on by default? → A: 30-day grace period after expiry. Hard-delete is off by default; operators opt in via `--hard-delete` flag on the prune command. Expired rows remain queryable for audit purposes until explicitly purged.
|
||||
- Q: `exports` disk storage backend — local disk or S3-compatible object storage? → A: Local disk (`storage/app/exports/`) with a persistent Docker volume mapped in Dokploy. Downloads streamed via signed route controller. S3 can be swapped later by changing the disk config without code changes.
|
||||
|
||||
---
|
||||
|
||||
## Assumptions
|
||||
|
||||
- `stored_reports`, `findings`, `operation_runs`, tenant, and workspace models exist per Specs 104/105/108; this spec adds no structural changes to those tables.
|
||||
- The `exports` filesystem disk is configured (or added as part of this spec) in `config/filesystems.php` as a private, non-public disk.
|
||||
- The `exports` filesystem disk is configured as a local private disk at `storage/app/exports/` in `config/filesystems.php`. In Dokploy deployments, this path MUST be mapped to a persistent Docker volume. S3-compatible storage can be substituted later by changing the disk driver without code changes.
|
||||
- The `OperationRun` active-run dedupe guard follows the same pattern established by prior specs; `tenant.review_pack.generate` is added to the run_type registry.
|
||||
- The canonical RBAC capability registry location is established by Specs 001/066; `REVIEW_PACK_VIEW` and `REVIEW_PACK_MANAGE` are added following the same pattern.
|
||||
- `principal.display_name` is the primary PII field in Entra admin roles stored_report payloads; additional PII fields follow the same redaction pattern if discovered.
|
||||
|
||||
Loading…
Reference in New Issue
Block a user