TenantAtlas/.specify/tasks.md

30 KiB
Raw Blame History

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)

  • T001 [P] [Shared] Confirm Sail/Env ready; ensure .env has PostgreSQL settings for Sail and Filament admin user seeded (if missing) in database/seeders/.
  • 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)

  • 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/.
  • T004 [Shared] Create models with relationships and guarded attributes for the above entities in app/Models/.
  • T005 [Shared] Implement Graph abstraction contracts (GraphClientInterface, error mapping, logging hooks) in app/Services/Graph/ with a mockable adapter.
  • T006 [Shared] Add audit logging service/helper to capture actor, tenant, operation, resources, outcome in app/Services/Intune/AuditLogger.php.
  • 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

  • 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

  • T009 [US1] Implement policy sync/import orchestrator using Graph abstraction in app/Services/Intune/PolicySyncService.php (no direct Graph in UI).
  • T010 [US1] Create Filament resource/table for policies with filters and metadata columns in app/Filament/Resources/PolicyResource.php.
  • 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

  • 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

  • T013 [US2] Implement backup domain service to assemble snapshots from policies with Graph payload retrieval in app/Services/Intune/BackupService.php.

  • T014 [US2] Add Filament resource/pages for backup sets and items (list/detail) in app/Filament/Resources/BackupSetResource.php.

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

  • T016 [P] [US3] Feature test for version capture and timeline display in tests/Feature/Filament/PolicyVersionTest.php.
  • T017 [P] [US3] Unit test for diff generation (human summary + JSON diff) in tests/Unit/VersionDiffTest.php.

Implementation for User Story 3

  • T018 [US3] Implement version capture service with immutable JSONB writes in app/Services/Intune/VersionService.php.
  • 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.
  • 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

  • T021 [P] [US4] Feature test for restore preview (change summary, conflicts, selective items) in tests/Feature/Filament/RestorePreviewTest.php.
  • 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

  • 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.
  • T024 [US4] Add Filament restore UI (wizard or pages) showing preview, warnings, and confirmation gate in app/Filament/Resources/RestoreRunResource.php.
  • 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

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

  • 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.
  • 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()).
  • 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“.
  • 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).
  • 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).
  • 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.
  • 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.
  • 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)

  • 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.
  • 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.
  • 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.
  • 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“).
  • 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.
  • 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

  • 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.
  • 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

  • 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.
  • 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.
  • 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.
  • T063 [HK] Filament-Delete-Action für PolicyVersions:

    • In PolicyVersionResource Delete-Action hinzufügen (List/Detail).
    • Confirmation + Audit-Log (policy_version.deleted).
  • 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

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

  • 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")

  • 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.
  • 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.
  • 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.
  • 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.
  • 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.
  • 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.

    • 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

  • 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
  • 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
  • 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
  • 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
  • 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

  • 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
  • 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
  • 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
  • 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
  • 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
  • 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:
      '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)
  • 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

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