# 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).