TenantAtlas/.specify/tasks.md

507 lines
30 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
description: "Task list for TenantPilot v1 implementation"
---
# Tasks: TenantPilot v1
**Input**: Design documents from `.specify/spec.md` and `.specify/plan.md`
**Prerequisites**: plan.md (complete), spec.md (complete)
**Status snapshot**
- Done: Phases 113 (US1US4, Settings normalization/display, Highlander, permissions/health, housekeeping/UX, ops)
- Next up: Phase 14 (US8) delegated Intune RBAC onboarding wizard (synchronous)
## Phase 1: Setup (Shared Infrastructure)
- [x] T001 [P] [Shared] Confirm Sail/Env ready; ensure `.env` has PostgreSQL settings for Sail and Filament admin user seeded (if missing) in `database/seeders/`.
- [x] T002 [P] [Shared] Add baseline docs for local dev and staging promotion notes in `README.md` (Sail commands, staging-before-prod reminder).
## Phase 2: Foundational (Blocking Prerequisites)
- [x] T003 [Shared] Add tenant-aware migrations for `tenants`, `policies`, `policy_versions`, `backup_sets`, `backup_items`, `restore_runs`, `audit_logs` with JSONB payloads and FK/time indexes in `database/migrations/`.
- [x] T004 [Shared] Create models with relationships and guarded attributes for the above entities in `app/Models/`.
- [x] T005 [Shared] Implement Graph abstraction contracts (`GraphClientInterface`, error mapping, logging hooks) in `app/Services/Graph/` with a mockable adapter.
- [x] T006 [Shared] Add audit logging service/helper to capture actor, tenant, operation, resources, outcome in `app/Services/Intune/AuditLogger.php`.
- [x] T007 [Shared] Seed supported policy types/metadata for initial scope in `database/seeders/PoliciesSeeder.php` and ensure tenant scoping.
## Phase 3: User Story 1 - Policy inventory listing (Priority: P1)
### Tests for User Story 1
- [x] T008 [P] [US1] Feature test for Filament policy listing and filtering (tenant-scoped) in `tests/Feature/Filament/PolicyListingTest.php` using mocked Graph sync.
### Implementation for User Story 1
- [x] T009 [US1] Implement policy sync/import orchestrator using Graph abstraction in `app/Services/Intune/PolicySyncService.php` (no direct Graph in UI).
- [x] T010 [US1] Create Filament resource/table for policies with filters and metadata columns in `app/Filament/Resources/PolicyResource.php`.
- [x] T011 [US1] Add command/job to sync policies (queues-ready) in `app/Console/Commands/SyncPolicies.php` and queue job under `app/Jobs/`.
## Phase 4: User Story 2 - Backup creation and browsing (Priority: P1)
### Tests for User Story 2
- [x] T012 [P] [US2] Feature test for creating backup sets with multiple policies and verifying immutable JSONB snapshots + audit log in `tests/Feature/Filament/BackupCreationTest.php`.
### Implementation for User Story 2
- [x] T013 [US2] Implement backup domain service to assemble snapshots from policies with Graph payload retrieval in `app/Services/Intune/BackupService.php`.
- [x] T014 [US2] Add Filament resource/pages for backup sets and items (list/detail) in `app/Filament/Resources/BackupSetResource.php`.
- [x] T131 [UX] [US2] Refactor BackupSet policy selection to RelationManager:
- Remove the multi-select policy picker from the BackupSet **Create** form (keep Create minimal: name/description).
- After create, redirect to BackupSet **Edit/View** where items can be managed.
- Add `BackupItemsRelationManager` to `BackupSetResource` showing a table with columns: Policy Name, Type (badge), Restore (badge), Risk (badge).
- Add header action “Policies hinzufügen” (searchable, multiple) that adds items/attaches policies **tenant-scoped** and prevents duplicates per BackupSet.
- Provide a remove action (detach/soft-delete as per domain rules).
- [x] T132 [P] [US2] Update/extend `tests/Feature/Filament/BackupCreationTest.php` to cover the new UX flow:
- Create BackupSet without policies.
- Add multiple policies via RelationManager action.
- Verify immutable JSONB snapshots + audit log behavior remains correct.
- [x] T015 [US2] Wire audit logging for backup creation events in `app/Services/Intune/BackupService.php` using `AuditLogger`.
## Phase 5: User Story 3 - Version history and diff (Priority: P1)
### Tests for User Story 3
- [x] T016 [P] [US3] Feature test for version capture and timeline display in `tests/Feature/Filament/PolicyVersionTest.php`.
- [x] T017 [P] [US3] Unit test for diff generation (human summary + JSON diff) in `tests/Unit/VersionDiffTest.php`.
### Implementation for User Story 3
- [x] T018 [US3] Implement version capture service with immutable JSONB writes in `app/Services/Intune/VersionService.php`.
- [x] T019 [US3] Create diff helper (summary + structured JSON) in `app/Services/Intune/VersionDiff.php` and surface in Filament version compare view in `app/Filament/Resources/PolicyVersionResource.php`.
- [x] T020 [US3] Hook version capture into relevant flows (manual trigger + backup/restore hooks) ensuring audit logging.
## Phase 6: User Story 4 - Restore with preview and confirmation (Priority: P1)
### Tests for User Story 4
- [x] T021 [P] [US4] Feature test for restore preview (change summary, conflicts, selective items) in `tests/Feature/Filament/RestorePreviewTest.php`.
- [x] T022 [P] [US4] Feature test for confirmed restore execution capturing audit logs and per-item outcomes in `tests/Feature/Filament/RestoreExecutionTest.php`.
### Implementation for User Story 4
- [x] T023 [US4] Implement restore service with preview/dry-run and selective item application in `app/Services/Intune/RestoreService.php`, integrating Graph adapter and conflict detection.
- [x] T024 [US4] Add Filament restore UI (wizard or pages) showing preview, warnings, and confirmation gate in `app/Filament/Resources/RestoreRunResource.php`.
- [x] T025 [US4] Record restore run lifecycle (start, per-item result, completion) and audit events in `restore_runs` and `audit_logs`.
## Phase 7: User Story 5 - Operational readiness and environments (Priority: P2)
### Implementation for User Story 5
- [x] T026 [US5] Document Dokploy staging→production promotion steps, required env vars, queue/worker expectations, and migration safety notes in `README.md` or `docs/deploy.md`.
- [x] T027 [US5] Add quick Sail commands and test invocation notes to `README.md` (e.g., `./vendor/bin/sail artisan test`) and ensure sample env entries for Graph credentials.
## Phase 8: User Story 6 - Tenant hinzufügen & Entra ID App-Setup (Priority: P1)
- [x] T030 [US6] Migration für `tenants` ergänzen/prüfen:
- Felder: `name`, `tenant_id` (GUID), `domain`, `app_client_id`, `app_status`, `app_notes`,
`created_at`, `updated_at`.
- Optional: Felder für Secret/Certificate-Config (verschlüsselt), falls benötigt.
- [x] T031 [US6] Eloquent Model `Tenant`:
- Beziehungen zu `policies`, `backup_sets`, `restore_runs`, `policy_versions`, `audit_logs`
über `tenant_id`.
- Tenant-aware Scopes, falls vorhanden (z. B. `forTenant()`).
- [x] T032 [US6] Filament-Resource `TenantResource`:
- Listenansicht: Name, Tenant ID, Domain, App-Status, erstellt/am.
- Create/Edit-Form: Name, Tenant ID, Domain, App-Client-ID, optionale Notizen.
- Detailseite mit Actions:
- „Open in Entra“ (Link zur App/Tenant im Entra-Portal),
- optional: „Copy Admin Consent URL“.
- [x] T033 [US6] `TenantConfigService` (oder Erweiterung des Graph-Clients):
- Methode `testConnectivity(Tenant $tenant)`: führt einen einfachen Graph-Call aus
(z. B. `/organization` oder ähnliches) mit den App-Daten des Tenants.
- Rückgabe: DTO/Array mit `success`, `error_message` (falls vorhanden).
- [x] T034 [US6] Action „Verify configuration“ in `TenantResource`:
- Ruft `testConnectivity()` auf,
- setzt `app_status` auf z. B. `ok`, `error` oder `consent_required`,
- zeigt eine Filament-Notification mit dem Ergebnis,
- schreibt einen Audit-Log-Eintrag (`tenant.config.verified`).
- [x] T035 [US6] Tenant-Kontext in bestehende Services integrieren:
- `PolicySyncService`, `BackupService`, `RestoreService` so anpassen,
dass sie einen `Tenant` oder `tenant_id` übergeben bekommen
und den Graph-Client mit diesem Kontext verwenden.
- Sicherstellen, dass alle policy/backup/restore/audit-Datensätze `tenant_id` setzen.
- [x] T036 [US6] Feature-Test `TenantSetupTest`:
- Erstellen eines Tenants via Filament (Create-Form).
- Aufruf der Action „Verify configuration“ mit gemocktem Graph-Client:
- einmal mit erfolgreichem Call → `app_status = ok`,
- einmal mit Fehler → `app_status = error` + passende Notification.
- Prüfen, dass Audit-Logs geschrieben werden.
- [x] T037 [US6] Admin-Consent Callback Route
- Route/Controller, der als `redirect_uri` der Entra-ID-App dient.
- Liest `tenant` / `error` / `admin_consent` aus der Query.
- Ordnet das dem richtigen `Tenant` zu (z. B. via `state`).
- Aktualisiert `app_status` (z. B. `ok`, `error`, `consent_denied`).
- Zeigt eine Bestätigungs-/Fehlerseite für den Admin.
---
## Phase 9: User Story 7 - Berechtigungsübersicht & Health-Status (Priority: P1)
- [x] T040 [US7] Zentrale Permissions-Liste anlegen:
- `config/intune_permissions.php` mit allen aktuell benötigten Graph-Berechtigungen:
- technischer Name (z. B. `DeviceManagementConfiguration.ReadWrite.All`),
- Typ: `application` / `delegated`,
- kurze Beschreibung,
- Feature-Tags (z. B. `["policy-sync", "backup"]`).
- Optional: `docs/permissions.md` mit einer Tabelle Feature ↔ Permission als
menschlich lesbare Referenz.
- [x] T041 [US7] Datenmodell für Tenant-Berechtigungen:
- Variante A (einfach): JSONB-Feld `granted_permissions` in `tenants` (Liste von Permission-Keys).
- Variante B (feiner): Tabelle `tenant_permissions` mit
`(tenant_id, permission_key, status, last_checked_at)`.
- `status` mindestens: `ok`, `missing`, `error`.
- [x] T042 [US7] Service `TenantPermissionService`:
- `getRequiredPermissions(): array` liest aus `config/intune_permissions.php`.
- `getGrantedPermissions(Tenant $tenant): array` liest aus Graph oder aus
`tenant_permissions`/`granted_permissions`.
- `compare(Tenant $tenant): TenantPermissionStatusDTO` liefert pro Permission
den Status (ok/missing/error) + Gesamthealth.
- [x] T043 [US7] Integration in Tenant-Detail-UI:
- Auf der `TenantResource`-Detailseite ein Panel/Section „Permissions“:
- Liste aller **required permissions**,
- pro Zeile: Name, Typ, Feature-Tags, Status (Icon + Label: OK/fehlt/Fehler).
- Optional: Link zu Doku oder Entra-Darstellung (z. B. „How to grant these permissions“).
- [x] T044 [US7] Action „Verify configuration“ erweitern:
- Zusätzlich zu `testConnectivity()` auch `TenantPermissionService::compare()` aufrufen.
- Ergebnisse in `tenant_permissions`/`granted_permissions` speichern.
- `app_status` und Permission-Health aktualisieren.
- Audit-Log-Eintrag `tenant.permissions.checked` schreiben.
- [x] T045 [US7] Tests für Permissions:
- Unit-Tests für `TenantPermissionService::compare()`:
- Szenarien: alle ok, Permission fehlt, Graph-Error.
- Feature-Test für Tenant-Detailseite:
- required permissions werden angezeigt,
- fehlende werden als fehlend markiert,
- „Verify configuration“ aktualisiert den Status wie erwartet.
## Phase 9b: Scope-Ausrichtung auf neue Objekttypen
- [x] T028 [Scope] Konfiguration `config/tenantpilot.php` auf die in `scope.supported_types` definierten Objekttypen erweitern (type/key, endpoint, label/category, optional risk/restore-Hinweis). Sicherstellen, dass diese Liste die einzige Quelle für Policy-Sync/Backup/Restore ist.
- [x] T029 [Scope] Filament-UI an neue Typen anpassen: Tabellenfilter/Grouping nach Kategorie (z.B. Config/Compliance/Scripts/Apps/CA), Backup/Restore-Formulare mit Hinweisen zu Restore-Level aus `scope.restore_matrix` (z.B. CA/enrollment restrictions = preview-only).
## Phase 10: Housekeeping Delete-Funktionen für Backups & Versions
- [x] T060 [HK] BackupSets soft deletable machen:
- `backup_sets` (und ggf. `backup_items`) Migration/Model mit `SoftDeletes` (deleted_at).
- Sicherstellen, dass RestoreRuns keine gelöschten BackupSets verwenden; Delete nur erlauben,
wenn keine zugehörigen RestoreRuns existieren.
- [x] T061 [HK] Filament-Delete-Action für BackupSets:
- In `BackupSetResource` Delete-Action in List- und/oder Detail-View hinzufügen.
- Mit Confirmation-Dialog (“This will archive this backup set and hide it from the UI.”).
- Delete disabled/hidden, wenn `restore_runs` für das Set existieren.
- Nach Delete Audit-Log (`backup.deleted`) schreiben.
- [x] T062 [HK] PolicyVersions soft deletable machen:
- `policy_versions` Migration/Model um `SoftDeletes` erweitern.
- Alle Queries und Filament-Resources so lassen, dass standardmäßig nur non-deleted Versions
angezeigt werden.
- [x] T063 [HK] Filament-Delete-Action für PolicyVersions:
- In `PolicyVersionResource` Delete-Action hinzufügen (List/Detail).
- Confirmation + Audit-Log (`policy_version.deleted`).
- [x] T064 [HK] Tests für Housekeeping:
- Feature-Test: Löschen eines BackupSets ohne RestoreRun → `deleted_at` gesetzt, UI-Eintrag weg,
Audit-Log vorhanden.
- Feature-Test: BackupSet mit RestoreRun → Delete-Action nicht verfügbar.
- Feature-Test: Löschen einer PolicyVersion → `deleted_at` gesetzt, nicht mehr in List sichtbar.
## Phase 11: Housekeeping Tenant löschen/deaktivieren
- [x] T070 [HK] Tenants soft deletable machen:
- `tenants` Model um `SoftDeletes` erweitern, Migration ggf. `deleted_at` hinzufügen.
- Optional: Feld `status` (enum/string: `active`, `archived`) einführen; beim Delete auf `archived` setzen.
- Alle Standard-Queries für Tenants nur `active` / nicht gelöscht anzeigen.
- [x] T071 [HK] Tenant-Delete-Action (Deaktivieren) in `TenantResource`:
- Delete-/Archive-Action in der Tenant-Liste und/oder Detailseite hinzufügen.
- Deutlich machen: “Deaktiviert diesen Tenant. Historische Daten bleiben vorhanden, neue Aktionen
sind nicht mehr möglich.”
- Bei Ausführung:
- `deleted_at` setzen (und ggf. `status = archived`),
- Audit-Log `tenant.deleted` oder `tenant.archived` schreiben.
- [x] T072 [HK] Verhalten für deaktivierte Tenants:
- In `PolicySyncService`, `BackupService`, `RestoreService` prüfen, dass nur aktive Tenants
verwendet werden; bei deaktiviertem Tenant frühzeitig mit verständlicher Fehlermeldung abbrechen.
- In Filament-Navigation Tenants, Policies, Backups, Restores eines deaktivierten Tenants nicht
mehr in Standard-Listen anzeigen (es sei denn, es gibt explizite “Show archived”-Filter).
- [x] T073 [HK] (Optional) RestoreRuns soft deletable machen:
- `restore_runs` Model/Migration mit `SoftDeletes`.
- Delete-Action in `RestoreRunResource` hinzufügen (nur UI-Aufräumung, keine Folgen für Backups).
- Audit-Log `restore_run.deleted` schreiben.
- [x] T074 [HK] Tests für Tenant-Delete:
- Feature-Test: Tenant löschen/deaktivieren → Tenant taucht nicht mehr in Standardlisten auf,
`deleted_at` (und `status`) ist gesetzt, Audit-Event existiert.
- Feature-Test: Versuch, mit deaktiviertem Tenant einen Policy-Sync/Backup/Restore zu starten,
führt zu einem klaren Fehler (und kein Graph-Call wird ausgeführt).
## Phase 12: Housekeeping Hard Deletes (Force Delete)
- [x] T075 [HK] Force-Delete-Actions ergänzen:
- Filament-Listen für Tenants, BackupSets, PolicyVersions, RestoreRuns erhalten „Force delete“
Aktionen (sichtbar nur im Trashed-Filter), mit klarer Confirmation.
- BackupSets: Force delete nur, wenn keine RestoreRuns existieren; löscht Items mit.
- Tenants: Force delete nur, wenn archiviert; blockiert für aktive Tenants.
- Alle Force-Deletes schreiben Audit-Log-Einträge vor der endgültigen Löschung.
- Tests für Force-Delete-Flows (erfolgreich/blockiert) ergänzen.
## Phase 12: Single current tenant ("Highlander")
- [x] T120 [TENANT] Migration `add_is_current_to_tenants`:
- Spalte `is_current` (boolean, default false, not null) zu `tenants` hinzufügen.
- Partielle Unique-Index anlegen, z. B.:
- `UNIQUE INDEX tenants_current_unique ON tenants (is_current)
WHERE is_current = true AND deleted_at IS NULL`.
- [x] T121 [TENANT] Tenant-Model anpassen:
- Methode `makeCurrent()` implementieren:
- Transaktion: alle anderen Tenants `is_current = false`, dieser Tenant `is_current = true`.
- Methode `static current()` implementieren:
- Wenn `INTUNE_TENANT_ID` gesetzt ist → Tenant mit dieser GUID laden,
sonst Exception, wenn nicht gefunden / deaktiviert.
- Wenn nicht gesetzt → Tenant mit `is_current = true` und `status = active`
(und `deleted_at` null) zurückgeben.
- Wenn keiner → Exception “No current tenant selected”.
- `findOrCreateDefault()` deprecaten/entfernen; keine Dummy-Tenants mehr erzeugen.
- [x] T122 [TENANT] Data-Migration / Cleanup:
- Falls mindestens ein Tenant mit `app_status = ok` existiert:
- einen als `is_current = true` markieren (z. B. den ersten).
- `local-tenant` auf `status = archived`, `is_current = false` setzen.
- Sicherstellen, dass `local-tenant` nie wieder als aktueller Kontext verwendet wird.
- [x] T123 [TENANT] Filament `TenantResource` UI:
- Spalte/Badge für `is_current` in der Liste hinzufügen.
- Table-Action "Make current" ergänzen:
- nur sichtbar für aktive Tenants, die nicht `is_current` sind.
- ruft `makeCurrent()` auf und zeigt Notification.
- Alte Logik entfernen, die `local-tenant` automatisch als Default nutzt.
- [x] T124 [TENANT] Consumers refactoren:
- Alle Vorkommen von `findOrCreateDefault()` suchen und durch `Tenant::current()`
(oder expliziten Tenant) ersetzen:
- Policy-Sync (Command + Filament-Action),
- BackupSet-Erstellung,
- RestoreRun-Erstellung,
- ggf. weitere Services.
- [x] T125 [TENANT] Tests:
- Unit-Tests für `Tenant::current()`:
- INTUNE_TENANT_ID gesetzt → nimmt diesen Tenant, Fehler wenn nicht vorhanden.
- INTUNE_TENANT_ID nicht gesetzt → nimmt den mit `is_current = true`.
- kein current Tenant → Exception.
- Feature-Test für "Make current" in `TenantResource`:
- Nach der Action ist genau ein Tenant `is_current = true`, alle anderen `false`.
- Optional: Test, dass `local-tenant` nach Cleanup nicht mehr als Kontext gewählt wird.
- [x] T130 [UX] Tabellen-Aktionen in Dropdown bündeln (ActionGroup)
- In `TenantResource` (Tenants-Liste) die Zeilen-Aktionen refaktorieren:
- `View` (optional) direkt anzeigen.
- Alle weiteren Aktionen (`Edit`, `Admin consent`, `Verify configuration`,
`Deactivate`, `Force delete`) in eine `Tables\Actions\ActionGroup` mit
"⋯"-Icon verschieben.
- Prüfen, ob in anderen Ressourcen mit vielen Row-Actions (z.B. Backups,
RestoreRuns) ebenfalls eine `ActionGroup` sinnvoll ist und diese konsistent
einsetzen.
## Phase 13: Settings Normalization & Display (Priority: P1)
**User Story**: US1b - "Admin can open a policy detail page and see the **effective Intune settings** in a readable, normalized way (not raw JSON dumps)"
**Prerequisites**: Phase 3 (Policy inventory) complete, PolicyResource and PolicyVersionResource exist
**Scope**: FR-019 / FR-019a / FR-019b, Edge-001 (malformed snapshots), Edge-002 (@odata.type mismatch), NFR-006 (normalization coverage) per .specify/plan.md dated 2025-12-10.
### Tests for User Story 1b
- [x] T140 [P] [US1b] Unit test for PolicyNormalizer service in `tests/Unit/PolicyNormalizerTest.php`:
- Test OMA-URI/custom policy transformation → path/value table structure
- Test Settings Catalog policy transformation → flattened key-value structure
- Test standard object transformation → labeled key-value with metadata filtered
- Test edge case: malformed snapshot (keys lost) → returns warning indicator
- Test edge case: @odata.type mismatch detection → returns mismatch flag
- [x] T141 [P] [US1b] Feature test for Settings section in Policy detail view in `tests/Feature/Filament/PolicySettingsDisplayTest.php`:
- Create policy with sample JSONB snapshot (OMA-URI, Settings Catalog, standard object)
- Visit Policy detail page via PolicyResource
- Assert "Settings" section exists using Infolist component
- Assert normalized settings are displayed (not raw JSON)
- Assert metadata keys (@odata.type, internal IDs) are hidden
- [x] T142 [P] [US1b] Feature test for pretty JSON + normalized settings in Version detail in `tests/Feature/Filament/PolicyVersionSettingsTest.php`:
- Create policy version with JSONB snapshot
- Visit PolicyVersion detail page via PolicyVersionResource
- Assert pretty-printed JSON exists (monospace, copyable format)
- Assert normalized settings section exists below JSON
- Assert both views show same data in different formats
- [x] T143 [P] [Edge] Feature test for malformed snapshot warning in `tests/Feature/Filament/MalformedSnapshotWarningTest.php`:
- Create policy version with malformed snapshot (array-only, keys lost)
- Visit detail pages (Policy and PolicyVersion)
- Assert UI warning displayed: "This snapshot may be incomplete or malformed"
- Assert partial settings display attempted with warning badge
- [x] T144 [P] [Edge] Feature test for @odata.type mismatch flag in `tests/Feature/Filament/ODataTypeMismatchTest.php`:
- Create policy with @odata.type that doesn't match expected platform/type mapping
- Visit Policy detail page
- Assert warning badge/banner displayed
- Create restore run with mismatched policy → assert validation error prevents execution
### Implementation for User Story 1b
- [x] T145 [US1b] Create PolicyNormalizer service in `app/Services/Intune/PolicyNormalizer.php`:
- Method `normalize(array $snapshot, string $policyType): array`:
- Returns `['status' => 'success|warning|error', 'settings' => [...], 'warnings' => [...]]`
- Implement OMA-URI transformation:
- Extract `omaSettings` array → convert to `[['path' => '...', 'value' => '...']]` table
- Filter metadata keys (@odata.type, id, version, etc.)
- Implement Settings Catalog transformation:
- Flatten nested `settings` or `settingsDelta` structures
- Convert to labeled key-value pairs with categories
- Implement standard object transformation:
- Extract top-level properties (displayName, description, etc.)
- Group by logical sections (General, Assignment, Advanced)
- Hide Graph-specific metadata
- Add edge case detection:
- Check for malformed snapshots (empty keys, string-only serialization)
- Check for @odata.type mismatch with policy type
- Return warnings array with human-readable messages
- [x] T146 [US1b] Add Settings Infolist section to PolicyResource ViewPolicy page in `app/Filament/Resources/PolicyResource/Pages/ViewPolicy.php`:
- Override `infolist()` method to add "Settings" section
- Use `Infolist\Components\Section` for grouping
- Call `PolicyNormalizer::normalize()` with policy's latest snapshot
- Render normalized settings using:
- `TextEntry` for simple key-value pairs
- `RepeatableEntry` for tables (OMA-URI paths)
- `KeyValueEntry` for nested structures
- Display warnings if normalization returns warning status
- Add fallback: "No settings available" if snapshot missing/empty
- [x] T147 [US1b] Enhance PolicyVersionResource ViewPolicyVersion with pretty JSON component in `app/Filament/Resources/PolicyVersionResource/Pages/ViewPolicyVersion.php`:
- Add "Snapshot" section with two subsections:
1. **Raw JSON** (collapsible):
- Use `ViewEntry` with custom view blade
- Format JSON with `json_encode($snapshot, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)`
- Wrap in `<pre><code class="language-json">` for syntax highlighting
- Add "Copy" button using Filament action
2. **Normalized Settings**:
- Call `PolicyNormalizer::normalize()` with version snapshot
- Render same structure as T146 (reuse component if possible)
- Handle edge case: malformed snapshot → show warning above both views
- [x] T148 [US1b] Integrate PolicyNormalizer output into existing views:
- Update `PolicyResource` list table: add hint column showing "Settings available" badge
- Update `PolicyVersionResource` diff view: optionally show normalized diff (if feasible)
- Ensure tenant-scoped queries when fetching policies for normalization
- Add caching consideration: normalize on-demand, don't store normalized output
- [x] T149 [Edge] Add snapshot validation helper in `app/Services/Intune/SnapshotValidator.php`:
- Method `validate(array $snapshot): ValidationResult`:
- Check for empty/null snapshot → return error
- Check for array-only structure (keys lost) → return warning
- Check for string-only serialization → return error
- Check for required Graph keys (@odata.type, displayName) → return warning if missing
- Return structured result with:
- `isValid: bool`
- `warnings: array`
- `errors: array`
- Use in PolicyNormalizer before transformation
- [x] T150 [Edge] Add @odata.type validator in Policy/BackupItem models:
- Add static method `Policy::validateODataType(array $snapshot, string $expectedType): bool`
- Check if `$snapshot['@odata.type']` matches expected Graph type for policy platform/type
- Create mapping array:
```php
'deviceConfiguration' => [
'windows' => '#microsoft.graph.windowsConfiguration',
'ios' => '#microsoft.graph.iosConfiguration',
// etc.
]
```
- Return bool + optional error message
- Call in BackupService before storing backup item
- Call in RestoreService before executing restore (gate check)
- [x] T151 [Edge] Display warnings in Filament UI for malformed/mismatched data:
- Update `PolicyResource/ViewPolicy` infolist:
- Add `Placeholder` component with warning icon + message if validation fails
- Show "⚠️ This snapshot may be incomplete or malformed" banner
- Update `PolicyVersionResource/ViewPolicyVersion`:
- Add warning badge next to "Snapshot" section title if malformed
- Update `RestoreRunResource` preview/execution:
- Add validation step that checks all selected items for @odata.type mismatch
- Show modal with list of problematic items + "Cancel" / "Proceed anyway" options
- Log validation warnings to audit log
- Ensure warnings are visible but not blocking (except for restore execution gate)
### Documentation for Phase 13
- [x] T152 [US1b] Update `README.md` with Settings normalization feature:
- Add section "Policy Settings Display" explaining:
- Normalized views for better readability
- Supported policy types (OMA-URI, Settings Catalog, standard objects)
- Edge case handling (malformed snapshots, type mismatches)
- Add screenshot/example of Settings section in Policy detail
- [x] T153 [US1b] Add inline documentation in PolicyNormalizer:
- PHPDoc blocks for all public methods
- Examples of input/output structures in comments
- Edge case handling notes
**Phase 13 Completion Criteria**:
- All 14 tasks (T140-T153) completed
- Tests passing for normalization logic and UI display
- Policy detail shows readable Settings section
- Version detail shows pretty JSON + normalized settings
- Edge cases handled with clear warnings
- Constitution Check: still 7/7 ✅ (auditability maintained, safety-first for restore gates)
## Phase 14: User Story 8 Intune RBAC Onboarding Wizard (Delegated, Synchronous)
**Scope**: FR-023 to FR-030; delegated login and grant run synchronously in Filament (no queue for grant). Optional jobs/CLI only for CHECK/REPORT (no grant).
- [ ] T160 [US8] Add TenantResource ActionGroup entry “Setup Intune RBAC”: visible for active tenants with `app_client_id`; sits alongside Admin consent/Verify; guarded by Highlander rules (no deactivated tenants).
- [ ] T161 [US8] Wizard UI (Filament): Step 1 preconditions/summary with inputs for Role (default Policy and Profile Manager, warn on Intune Admin), Scope (All devices or scope group), Group mode (create default TenantPilot-Intune-RBAC vs select existing security-enabled group); surface least-privilege warning for production.
- [ ] T162 [US8] Delegated auth step: initiate delegated login for selected tenant; stop with clear error + audit on failure/denied consent; short-lived token only (no storage).
- [ ] T163 [US8] Execution service (synchronous) with audit per step: resolve service principal by `app_client_id`; ensure/create security group; add SP as member (idempotent); ensure/create/update Intune role assignment for chosen role/scope using Graph abstraction; no queue usage.
- [ ] T164 [US8] Post-check (mandatory): force fresh token acquisition (clear tenant token cache) and run canary reads:
- `GET /deviceManagement/deviceConfigurations?$top=1`
- `GET /deviceManagement/deviceCompliancePolicies?$top=1`
- Optional: `GET /identity/conditionalAccess/policies?$top=1` only if CA features are enabled
- Update `app_status`/permissions health + write audit entries (start/login/group/member/assignment/verify outcomes).
- [ ] T165 [US8] Tests (Pest, mocked Graph): happy path; rerun idempotent (no duplicates); missing permissions → clear error mapping; scope-limited selection → warning surfaced; delegated login failure path.
- [ ] T166 [US8] Documentation: README note for wizard behavior (delegated, synchronous), least-privilege defaults, audit expectations, and how to rerun safely.
- [ ] T167 [US8-Optional] CLI/Job for CHECK/REPORT only (no grant) to inspect RBAC state; explicitly exclude grant actions from async/queue.
- [ ] T168 [US8] Extend Verify configuration / Health panel to include “Intune RBAC status”:
- Determine whether the configured Enterprise App (service principal) is covered by an Intune role assignment for required scopes (OK / Missing / Error).
- Show actionable message (“Run Setup Intune RBAC”) when missing.
- Persist last_checked_at + short reason code; log audit `tenant.rbac.checked`.
- [ ] T169 [US8] Persist RBAC artifacts on Tenant for stable idempotency:
- Add nullable columns to `tenants`: `rbac_group_id`, `rbac_role_assignment_id`, `rbac_role_key`, `rbac_scope_mode`, `rbac_scope_id`.
- Prefer stored IDs on reruns; fall back to discovery only if missing.
- Add migration + model casts; include IDs in audit context (IDs only; no secrets).