Implements provider access hardening for Intune write operations: - RBAC-based write gate with configurable staleness thresholds - Gate enforced at restore start and in jobs (execute + assignments) - UI affordances: disabled rerun action, tenant RBAC status card, refresh RBAC action - Audit logging for blocked writes - Ops UX label: `rbac.health_check` now displays as “RBAC health check” - Adds/updates Pest tests and SpecKit artifacts for feature 108 Notes: - Filament v5 / Livewire v4 compliant. - Destructive actions require confirmation. - Assets: no new global assets. Tested: - `vendor/bin/sail artisan test --compact` (suite previously green) + focused OpsUx tests for OperationCatalog labels. - `vendor/bin/sail bin pint --dirty`. Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #132
9.9 KiB
Implementation Plan: Provider Access Hardening v1 — Intune Write Gate
Branch: 108-provider-access-hardening | Date: 2026-02-22 | Spec: specs/108-provider-access-hardening/spec.md
Input: Feature specification from specs/108-provider-access-hardening/spec.md
Note: This template is filled in by the /speckit.plan command. See .specify/scripts/ for helper scripts.
Summary
Add a defense-in-depth, server-side write gate that blocks all Intune write operations (restore execution + restore assignments) unless tenant RBAC hardening is configured, healthy, and fresh. The gate reads only persisted Tenant RBAC status fields (no synchronous Graph calls), returns stable reason codes, disables Filament write actions with explanations, and fails queued jobs safely before any Graph mutation.
Technical Context
Language/Version: PHP 8.4.x (Laravel 12)
Primary Dependencies: Filament v5 + Livewire v4, Laravel Sail, Microsoft Graph via GraphClientInterface
Storage: PostgreSQL (Sail)
Testing: Pest v4 (PHPUnit 12 underneath)
Target Platform: Web app (Laravel) running in Docker via Sail
Project Type: Web application (backend-rendered admin via Filament)
Performance Goals: Gate evaluation is O(1) DB reads; no Graph calls; negligible overhead per write attempt
Constraints: Must be DB-only at evaluation time; stable reason codes; no secrets in logs; no UI render-time HTTP
Scale/Scope: Tenant-scoped; affects existing write start surfaces + queued jobs only (no new Resources/Pages)
Constitution Check
GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.
Status: PASS (no violations expected).
- Inventory-first: clarify what is “last observed” vs snapshots/backups
- Read/write separation: any writes require preview + confirmation + audit + tests
- Graph contract path: Graph calls only via
GraphClientInterface+config/graph_contracts.php - Deterministic capabilities: capability derivation is testable (snapshot/golden tests)
- RBAC-UX: two planes (/admin vs /system) remain separated; cross-plane is 404; tenant-context routes (/admin/t/{tenant}/...) are tenant-scoped; canonical workspace-context routes under /admin remain tenant-safe; non-member tenant/workspace access is 404; member-but-missing-capability is 403; authorization checks use Gates/Policies + capability registries (no raw strings, no role-string checks)
- Workspace isolation: non-member workspace access is 404; tenant-plane routes require an established workspace context; workspace context switching is separate from Filament Tenancy
- RBAC-UX: destructive-like actions require
->requiresConfirmation()and clear warning text - RBAC-UX: global search is tenant-scoped; non-members get no hints; inaccessible results are treated as not found (404 semantics)
- Tenant isolation: all reads/writes tenant-scoped; cross-tenant views are explicit and access-checked
- Run observability: long-running/remote/queued work creates/reuses
OperationRun; start surfaces enqueue-only; Monitoring is DB-only; DB-only <2s actions may skip runs but security-relevant ones still audit-log; auth handshake exception OPS-EX-AUTH-001 allows synchronous outbound HTTP on/auth/*withoutOperationRun - Automation: queued/scheduled ops use locks + idempotency; handle 429/503 with backoff+jitter
- Data minimization: Inventory stores metadata + whitelisted meta; logs contain no secrets/tokens
- Badge semantics (BADGE-001): status-like badges use
BadgeCatalog/BadgeRenderer; no ad-hoc mappings; new values include tests - Filament UI Action Surface Contract: for any new/modified Filament Resource/RelationManager/Page, define Header/Row/Bulk/Empty-State actions, ensure every List/Table has a record inspection affordance (prefer
recordUrl()clickable rows; do not render a lone View row action), keep max 2 visible row actions with the rest in “More”, group bulk actions, require confirmations for destructive actions (typed confirmation for large/bulk where applicable), write audit logs for mutations, enforce RBAC via central helpers (non-member 404, member missing capability 403), and ensure CI blocks merges if the contract is violated or not explicitly exempted - Filament UI UX-001 (Layout & IA): Create/Edit uses Main/Aside (3-col grid, Main=columnSpan(2), Aside=columnSpan(1)); all fields inside Sections/Cards (no naked inputs); View uses Infolists (not disabled edit forms); status badges use BADGE-001; empty states have specific title + explanation + 1 CTA; max 1 primary + 1 secondary header action; tables provide search/sort/filters for core dimensions; shared layout builders preferred for consistency
Project Structure
Documentation (this feature)
specs/108-provider-access-hardening/
├── plan.md # This file (/speckit.plan command output)
├── research.md # Phase 0 output (/speckit.plan command)
├── data-model.md # Phase 1 output (/speckit.plan command)
├── quickstart.md # Phase 1 output (/speckit.plan command)
├── contracts/ # Phase 1 output (/speckit.plan command)
└── tasks.md # Phase 2 output (/speckit.tasks command - NOT created by /speckit.plan)
Source Code (repository root)
app/
├── Filament/
│ ├── Resources/
│ │ ├── RestoreRunResource.php
│ │ └── TenantResource.php
│ └── Resources/TenantResource/Pages/
│ └── ViewTenant.php
├── Jobs/
│ ├── ExecuteRestoreRunJob.php
│ └── RestoreAssignmentsJob.php
├── Models/
│ ├── Tenant.php
│ ├── OperationRun.php
│ └── AuditLog.php
├── Services/
│ └── Providers/
│ └── ProviderOperationStartGate.php
└── Support/
└── (badge domains, reason codes, UI enforcement helpers)
tests/
├── Feature/
└── Unit/
Structure Decision: Web application (Laravel) with Filament/Livewire admin panel. Gate logic lives in application services and is invoked from Filament start surfaces and queued jobs.
Complexity Tracking
Fill ONLY if Constitution Check has violations that must be justified
| Violation | Why Needed | Simpler Alternative Rejected Because |
|---|---|---|
| [e.g., 4th project] | [current need] | [why 3 projects insufficient] |
| [e.g., Repository pattern] | [specific problem] | [why direct DB access insufficient] |
Phase 0 — Outline & Research
Output: specs/108-provider-access-hardening/research.md
Research focus for this feature is mostly decision capture + aligning with existing patterns:
- Confirm start surfaces and jobs involved (RestoreRun execution + assignments restore)
- Confirm the existing RBAC setup surface (Tenant view /
TenantResource::rbacAction()) - Confirm existing observability primitives (
OperationRun) and audit logging (AuditLog) - Decide on behavior for in-flight health check and for gate toggle (bypass + logging)
Phase 1 — Design & Contracts
Outputs:
- specs/108-provider-access-hardening/data-model.md
- specs/108-provider-access-hardening/contracts/intune-write-gate.openapi.yaml
- specs/108-provider-access-hardening/quickstart.md
Design highlights:
- Introduce a provider-agnostic gate interface with an Intune implementation (
IntuneRbacWriteGate). - Gate reads only persisted Tenant RBAC fields (
rbac_status,rbac_last_checked_at,rbac_status_reason). - Gate returns stable reason codes:
intune_rbac.not_configured,intune_rbac.unhealthy,intune_rbac.stale. - Start surfaces:
RestoreRunResource::createRestoreRun()blocks before creatingOperationRun+ before enqueuingExecuteRestoreRunJob.- Any “rerun/execute” action path in
RestoreRunResourceblocks similarly before dispatch.
- Job-level enforcement:
ExecuteRestoreRunJob::handle()re-checks gate before calling restore service.RestoreAssignmentsJob::handle()re-checks gate before callingAssignmentRestoreService.
- UI affordance:
- Tenant view RBAC infolist section is collapsed into a compact status card with contextual actions.
- Write-trigger actions render visible but disabled with tooltips when gate would block.
Post-design Constitution Re-check
Status: PASS (design remains DB-only for gate evaluation, does not add Graph calls/contracts, preserves RBAC semantics, and keeps destructive actions confirmed + audited).
Phase 1 — Agent Context Update
Run: .specify/scripts/bash/update-agent-context.sh copilot
Phase 2 — Implementation Planning (for /speckit.tasks)
Implementation tasks will be generated in tasks.md via /speckit.tasks, but the intended breakdown is:
- Add gate service + exception (reason codes + message mapping)
- Start-surface enforcement (RestoreRun start + rerun + assignments start)
- Job-level enforcement (ExecuteRestoreRunJob, RestoreAssignmentsJob)
- Tenant view RBAC card + action disabling + CTAs
- Audit logging for blocked attempts (use existing
AuditLog) - Add/adjust badge semantics for
staleinTenantRbacStatus - Pest tests: start surfaces + jobs + UI disabled state + reason codes