# 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](specs/108-provider-access-hardening/spec.md)
**Input**: Feature specification from [specs/108-provider-access-hardening/spec.md](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

<!--
  ACTION REQUIRED: Replace the content in this section with the technical details
  for the project. The structure here is presented in advisory capacity to guide
  the iteration process.
-->

**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/*` without `OperationRun`
- 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)

```text
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)
<!--
  ACTION REQUIRED: Replace the placeholder tree below with the concrete layout
  for this feature. Delete unused options and expand the chosen structure with
  real paths (e.g., apps/admin, packages/something). The delivered plan must
  not include Option labels.
-->

```text
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](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/data-model.md)
- [specs/108-provider-access-hardening/contracts/intune-write-gate.openapi.yaml](specs/108-provider-access-hardening/contracts/intune-write-gate.openapi.yaml)
- [specs/108-provider-access-hardening/quickstart.md](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 creating `OperationRun` + before enqueuing `ExecuteRestoreRunJob`.
  - Any “rerun/execute” action path in `RestoreRunResource` blocks similarly before dispatch.
- Job-level enforcement:
  - `ExecuteRestoreRunJob::handle()` re-checks gate before calling restore service.
  - `RestoreAssignmentsJob::handle()` re-checks gate before calling `AssignmentRestoreService`.
- 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:

1) Add gate service + exception (reason codes + message mapping)
2) Start-surface enforcement (RestoreRun start + rerun + assignments start)
3) Job-level enforcement (ExecuteRestoreRunJob, RestoreAssignmentsJob)
4) Tenant view RBAC card + action disabling + CTAs
5) Audit logging for blocked attempts (use existing `AuditLog`)
6) Add/adjust badge semantics for `stale` in `TenantRbacStatus`
7) Pest tests: start surfaces + jobs + UI disabled state + reason codes
