# Research — Spec 076 (Permissions Enterprise UI)

## Decisions

### 1) Build a dedicated tenant-scoped Filament Page
- Decision: Implement a new Filament Page under the tenant panel route (`/admin/t/{tenant}/...`) for the “Required permissions” enterprise remediation UX.
- Rationale:
  - The existing `TenantResource` infolist entry is a raw list; Spec 076 requires a two-layer remediation layout (overview + matrix) and feature grouping.
  - A dedicated Page can provide operator-first UX without bloating the tenant detail resource.
- Alternatives considered:
  - Extending the existing `TenantResource` view: rejected because it couples a complex remediation UI to a general-purpose resource view and makes verification deep-linking/clustering harder.

### 2) Use stored + config-based data only at render time (DB-only render)
- Decision: The page loads required permissions from `config('intune_permissions.permissions')` and granted/missing statuses from the `tenant_permissions` table via `TenantPermissionService::compare($tenant, persist: false, liveCheck: false, useConfiguredStub: false)`.
- Rationale:
  - Satisfies FR-076-008 (no external network calls during page view).
  - Reuses existing data normalization and status modeling.
- Alternatives considered:
  - Calling Graph on page view (`liveCheck: true`): rejected (explicitly out of scope and violates DB-only render).

### 3) Authorization semantics: non-member 404, member missing capability 403
- Decision:
  - Non-member tenant access remains deny-as-not-found (404) via the Admin panel middleware (`DenyNonMemberTenantAccess`).
  - Page access is capability-gated via `Page::canAccess()` using `Capabilities::TENANT_VIEW`.
- Rationale:
  - Matches Constitution RBAC-UX-002 and RBAC-UX-003.
  - Ensures correct semantics for both initial request and Livewire requests.
- Alternatives considered:
  - Enforcing capability only in `mount()` with custom `abort(...)`: rejected because `canAccess()` is the consistent Filament entry-point gate and keeps nav hiding in sync.

### 4) Badge semantics: use centralized domains only
- Decision:
  - Per-permission badges: `BadgeDomain::TenantPermissionStatus`.
  - Overall status badge: `BadgeDomain::VerificationReportOverall` with values from `VerificationReportOverall`.
- Rationale:
  - Constitution BADGE-001 requires centralized semantic mapping.

### 5) Filters/search implementation: server-side Livewire state, in-memory filtering
- Decision: Represent filter/search state as Livewire properties on the Page and filter the already-loaded permission array in-memory.
- Rationale:
  - Dataset size is small (config-defined permissions), so in-memory filtering is fast and stable.
  - Enables programmatic tests for filtering/copy payload generation without relying on browser JS.
- Alternatives considered:
  - Filament `Tables` with query-backed filters: rejected as unnecessary complexity for a config-driven list.
  - Pure client-side Alpine filtering: rejected due to weaker automated testability.

### 6) Copy-to-clipboard: “copy payload modal” + robust clipboard fallback
- Decision: Implement copy actions that open a modal (or inline panel) containing the exact newline-separated payload; a “Copy” button uses the existing robust Alpine clipboard fallback pattern.
- Rationale:
  - Clipboard APIs are browser-only; Livewire actions cannot write directly to clipboard.
  - Reuses proven fallback approach in `resources/views/filament/partials/json-viewer.blade.php`.
  - Makes copy output auditable/visible before copying (enterprise-friendly).
- Alternatives considered:
  - Attempting server-side copy: not possible.

### 7) Verify-step clustering: emit clustered checks in `verification_report`
- Decision:
  - Extend the queued verification job (`ProviderConnectionHealthCheckJob`) to write a verification report that includes the existing connection check plus 5–6 permission cluster checks.
  - Update the onboarding wizard Verify step to render `OperationRun.context.verification_report` (via `VerificationReportViewer`) and show checks issues-first.
- Rationale:
  - The project already has a report schema (`VerificationReportSchema`) and writer (`VerificationReportWriter`).
  - Clustering in the report keeps the experience consistent across the wizard and operation-run detail views.
- Alternatives considered:
  - Cluster in Blade only: rejected because it does not affect summary/overall and can drift between views.

### 8) Enterprise correctness: refresh Observed permissions during the verification run
- Decision:
  - The queued verification run (Operation Run) attempts a live Graph refresh for Observed permissions and persists it to `tenant_permissions`.
  - Viewer surfaces (Required Permissions page, onboarding Verify step, operation run viewer) remain DB-only at render time.
  - If the refresh fails (429/network), permission clusters degrade to warnings with retry guidance and MUST NOT assert “missing permissions” based on stale/empty inventory.
- Rationale:
  - Prevents false “missing” findings when the stored inventory is empty/stale.
  - Keeps all external calls in the queued run, maintaining the DB-only render rule.

## Open questions resolved (NEEDS CLARIFICATION → decision)

### “Enabled features” for impact summary
- Decision: In Spec 076 scope, treat “enabled features” as the feature tags present in `config('intune_permissions.permissions')`.
- Rationale: There is no current per-tenant feature-enable registry in the codebase; feature tags already exist and are deterministic.
- Future upgrade path: If/when tenant-specific enablement exists, compute relevance by intersecting enabled features with permission feature tags.

## Check cluster proposal (stable keys)

Target: 5–7 checks; issues-first.

- `provider.connection.check` (existing)
- `permissions.admin_consent` (overall admin consent / application permissions missing)
- `permissions.directory_groups`
- `permissions.intune_configuration`
- `permissions.intune_apps`
- `permissions.intune_rbac_assignments`
- `permissions.scripts_remediations` (optional / skip when irrelevant)

Each permission-derived check:
- Pass: no missing permissions in its mapped set
- Fail/Blocked: any missing required permission in its set
- Skip: cluster mapped permissions set is empty (or feature not relevant)
- Next step: “Open required permissions” deep link to the new page (optionally pre-filtered by Feature).
