TenantAtlas/specs/001-rbac-onboarding/tasks.md
2025-12-14 19:56:02 +01:00

52 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)


Constitution Evidence Ledger (Discovery + Verification)

This section is the canonical evidence record to satisfy Constitution VII (Spec-Driven Development) and IV (Auditability). Each completed phase has: (a) discovery notes, (b) verification commands, (c) where to look in repo/UX.

Evidence: Phases 16 (US1US4 core)

  • Discovery: Verified existing Filament resources and services implement tenant scoping and Graph abstraction; restore flow includes preview + confirmation; versions stored immutable JSONB; audits written for critical operations.
  • Verification:
    • ./vendor/bin/pest tests/Feature/Filament/PolicyListingTest.php
    • ./vendor/bin/pest tests/Feature/Filament/BackupCreationTest.php
    • ./vendor/bin/pest tests/Feature/Filament/RestorePreviewTest.php tests/Feature/Filament/RestoreExecutionTest.php
    • ./vendor/bin/pest tests/Feature/Filament/PolicyVersionTest.php tests/Unit/VersionDiffTest.php
    • ./vendor/bin/pint --dirty
  • Manual checks: Filament UI: Policies list/filter, BackupSet detail + items, RestoreRun preview/execution, PolicyVersion view/diff.

Evidence: Phase 13 (US1b settings display + safety gates)

  • Discovery: Normalized settings display added; malformed snapshot warnings; @odata.type mismatch gates block restore execution.
  • Verification:
    • ./vendor/bin/pest tests/Unit/PolicyNormalizerTest.php
    • ./vendor/bin/pest tests/Feature/Filament/PolicySettingsDisplayTest.php
    • ./vendor/bin/pest tests/Feature/Filament/PolicyVersionSettingsTest.php
    • ./vendor/bin/pint --dirty

Evidence: Phase 14 (US7 RBAC wizard)

  • Discovery: RBAC wizard stack present (TenantResource action, delegated auth controller, onboarding service, health panel, migrations, tests).
  • Verification:
    • ./vendor/bin/pest tests/Feature/Filament/TenantRbacWizardTest.php
    • ./vendor/bin/pest tests/Unit/RbacOnboardingServiceTest.php
    • ./vendor/bin/pint --dirty

Evidence: Phase 15 (US8 Graph Contract Registry & Drift Guard)

  • Discovery: Contract registry + fallback integrated in Graph client; drift-check command added; type-family tolerant @odata validation added.
  • Verification:
    • ./vendor/bin/pest tests/Unit/GraphContractRegistryTest.php tests/Unit/GraphContractFallbackTest.php
    • ./vendor/bin/pest tests/Feature/Filament/ODataTypeMismatchTest.php
    • ./vendor/bin/pint --dirty

Evidence: Settings Catalog (settingsCatalogPolicy) extensions

  • Discovery: Added first-class sync/type + restore hardening + hydration + normalized display improvements.
  • Verification:
    • ./vendor/bin/pest tests/Feature/Filament/SettingsCatalogPolicySyncTest.php
    • ./vendor/bin/pest tests/Feature/Filament/SettingsCatalogRestoreTest.php
    • ./vendor/bin/pest tests/Feature/Filament/SettingsCatalogPolicyHydrationTest.php
    • ./vendor/bin/pest tests/Unit/PolicyNormalizerSettingsCatalogTest.php
    • ./vendor/bin/pint --dirty

FR → Tasks Traceability Matrix (Explicit)

This matrix makes FR coverage explicit (tooling/audits). Tasks also carry local Implements: tags where most useful.

  • FR-001 (Inventory) → T008T011
  • FR-002 (Backups) → T012T015, T131T132
  • FR-003 (Auditability baseline) → T006, T015, T020, T025
  • FR-004 (Versions) → T016T020
  • FR-005 (Diffs) → T017, T019
  • FR-006FR-010 (Restore safety + preview + gating) → T021T026, T144, T151
  • FR-011FR-018 (Tenant-aware + Graph abstraction + governance basics) → T003T007, T035, T120T125
  • FR-019.1FR-019.2 (Settings normalization + edge cases) → T140T153
  • FR-023FR-030 (RBAC onboarding wizard) → T160T169
  • FR-031FR-034 (Contract registry + drift guard) → T170T175
  • FR-035 (Rerun restore) → T156

Tasks: TenantPilot v1

Measurable Thresholds (NFR/UX)

These thresholds make qualitative terms measurable and testable.

Payload / Rendering Limits

  • Settings table max rows: 1000 rows per rendered table block (truncate with notice).
  • Flatten recursion depth: max depth 8; if exceeded, stop and warn.
  • Max value length: 500 characters rendered inline; provide copy/full view for longer values.
  • Max JSON pretty print: 1 MB rendered inline; above that show “download/copy only”.

Graph Request Limits

  • Default Graph request timeout: 30s per request.
  • Hydration pagination limit: max 50 pages per subresource; if exceeded → warning + partial snapshot.

Restore Safety

  • Dry-run is binary: a restore run is either dry-run or execute; no “default dry-run=true” semantics.
  • Type mismatch gate: @odata.type mismatch MUST block execution (preview may show).

Retention / Housekeeping

  • Soft-deleted entities: retained indefinitely unless explicitly force-deleted.
  • Audit logs: retained indefinitely by default (configurable later).

“Large payload” definition

  • Any snapshot JSONB > 1 MB OR settings table > 1000 rows is considered large and triggers truncation rules above.

FR-019 Settings Normalization & Display

FR-019.1 Normalized Settings View

  • Admin can view a policy and policy version with settings rendered in a readable normalized format.
  • The normalized output MUST hide Graph metadata keys unless explicitly requested.

FR-019.2 Raw Snapshot + Copy

  • Admin can view raw JSON snapshot (pretty-printed where possible) and copy it.

FR-019.3 Edge Handling

  • Malformed snapshots MUST show a warning banner and attempt partial rendering.
  • @odata.type mismatch MUST show a warning; restore execution MUST be blocked.

FR-019.4 Thresholds

  • Rendering and snapshot size limits MUST follow “Measurable Thresholds (NFR/UX)”.

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

    • Implements: FR-006, FR-008, FR-009
    • Implements: FR-021
    • Implements: FR-020
    • Verified by: ./vendor/bin/pest tests/Feature/Filament/RestorePreviewTest.php tests/Feature/Filament/RestoreExecutionTest.php
  • T145 [US1b] Create PolicyNormalizer service in app/Services/Intune/PolicyNormalizer.php.

    • Implements: FR-019.1, FR-019.3
    • Verified by: ./vendor/bin/pest tests/Unit/PolicyNormalizerTest.php
  • T160 [US7] Add TenantResource ActionGroup entry “Setup Intune RBAC” …

    • Implements: FR-023, FR-024
    • Verified by: ./vendor/bin/pest tests/Feature/Filament/TenantRbacWizardTest.php
  • T170 [US8] Add contract registry artifact …

    • Implements: FR-031
    • Verified by: ./vendor/bin/pest tests/Unit/GraphContractRegistryTest.php (See above for file contents. You may not need to search or read the file again.)

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/.
    • Implements: FR-013
  • 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.

  • T176 [Scope][US1] Add Settings Catalog Policies as first-class type (settingsCatalogPolicy)

    • Goal: Intune Settings Catalog Policies werden als eigener Typ synchronisiert, angezeigt und sind für Backup/Version/Diff/Preview/Restore (gemäß Matrix) korrekt routbar.
    • Why: Settings Catalog Policies liegen in Graph unter deviceManagement/configurationPolicies (nicht unter deviceManagement/deviceConfigurations). Aktuell erscheinen sie daher nicht (oder nur unvollständig).

    Implementation

    1. Config: supported_types erweitern (Single Source of Truth)

      • In config/tenantpilot.php (oder eurem zentralen Type-Registry-File) neuen Typ hinzufügen:
        • key: settingsCatalogPolicy
        • name: Settings Catalog Policy
        • graph_resource: deviceManagement/configurationPolicies
        • category: Configuration
        • platform: windows (oder all + später per snapshot/@odata ableiten je nach eurer Modelllogik)
      • UI-Label so wählen, dass Admin sofort erkennt: “Settings Catalog” (z. B. Badge/Label).
    2. Restore-Matrix erweitern

      • In eurer Restore-Konfig (scope.restore_matrix bzw. config-driven Matrix):
        • settingsCatalogPolicy: backup: full, restore: enabled, risk: medium (optional medium-high falls ihr strenger sein wollt)
      • Restore-Warnungen/Badges müssen den neuen Typ korrekt anzeigen.
    3. Graph Contract Registry erweitern

      • In config/graph_contracts.php Contract für settingsCatalogPolicy hinzufügen:
        • Resource paths (collection + single item)
        • allowed_select/allowed_expand (konservativ starten)
        • type_family / erlaubte @odata.type Werte für diesen Typ
        • Create/Update routing (POST/PATCH wie bei euren anderen Typen)
      • Sicherstellen, dass capability fallback (downgrade ohne $select/$expand) auch hier greift.
    4. PolicySyncService erweitern

      • Sync-Pipeline muss zusätzlich deviceManagement/configurationPolicies abfragen und upserten:
        • policies.type_key = settingsCatalogPolicy
        • external_id = Graph id
        • display_name, description, last_modified, etc.
      • Tenant-scoping beibehalten.
      • No duplicates: gleiche external_id darf nicht in zwei Typen landen (Unique/Guard prüfen).
    5. Snapshots / Settings availability

      • Für die Spalte/Badge “Settings” (Available/Missing):
        • Snapshot-Fetch muss für settingsCatalogPolicy über den neuen Endpoint laufen (single item fetch).
      • Normalizer/Validator:
        • @odata.type muss für diesen Typ als kompatibel erkannt werden (über Contract/type-family).
    6. UI (Filament)

      • PolicyResource:
        • Type/Category Filter um Settings Catalog Policy erweitern
        • Optional: Category bleibt Configuration, aber Typ klar Settings Catalog
      • Detailseite:
        • Normalized Settings anzeigen (wenn euer Normalizer Settings Catalog schon kann)
        • sonst mind.: Raw JSON + Hinweis “Settings Catalog normalization pending” (kein silent fail).
    7. Permissions/Health

      • Verify/Permissions-Liste prüfen, ob für deviceManagement/configurationPolicies zusätzliche Graph-Permissions nötig sind.
      • Falls ja:
        • config/intune_permissions.php ergänzen
        • Health Panel zeigt fehlende Permission sauber an.

    Tests (Pest)

    • Unit:
      • Contract Registry erkennt settingsCatalogPolicy
      • type-family ok (derived @odata.type accepted)
      • fallback ok (capability downgrade)
    • Feature:
      • Policy Sync importiert configurationPolicies als settingsCatalogPolicy und listet sie in der UI
      • Settings badge wird Available, sobald Snapshot vorhanden ist
    • Regression:
      • deviceConfiguration Sync bleibt unverändert (keine Vermischung)

    Verification

    • ./vendor/bin/pest tests/Feature/Filament/PolicyListingTest.php
    • ggf. neue Tests:
      • ./vendor/bin/pest tests/Feature/Filament/SettingsCatalogPolicySyncTest.php
    • Registry Tests erweitern:
      • ./vendor/bin/pest tests/Unit/GraphContractRegistryTest.php

    Acceptance Criteria

    • In der Policies-Liste erscheinen Intune Settings Catalog Policies als eigener Typ Settings Catalog Policy.
    • Admin kann danach Backup/Version/Preview/Restore (gemäß Matrix) für diesen Typ nutzen.
    • Keine Duplikate/Überlappung mit deviceConfiguration.
  • T177 [US4][Bugfix] Settings Catalog Restore: Graph-Fehlerdetails speichern + PATCH-Payload sanitizen (contract-driven)

    • Goal: Restore von settingsCatalogPolicy soll nicht mehr als generisches 400 Graph apply failed enden, sondern:
      1. echte Graph-Fehlerdetails persistieren + im UI sichtbar machen
      2. beim PATCH nur ein zulässiges Payload senden (read-only/meta Felder raus, whitelist/contract-driven)
    • Why: deviceManagement/configurationPolicies akzeptiert beim PATCH i. d. R. keinen vollständigen Snapshot → read-only Felder führen zu 400.

    Implementation

    1. RestoreRun Results verbessern (Fehlerdetails persistieren)

      • In RestoreService (oder zentralem Graph-Apply Catch):
        • Bei Graph-Exception zusätzlich in restore_run_item_results/results JSON speichern:
          • graph_error_code
          • graph_error_message
          • optional (falls vorhanden): graph_request_id, graph_client_request_id, graph_date
        • UI (RestoreRun Detail) soll bei failed Items neben code/reason auch graph_error_message anzeigen (kurz) + “Details” (expand/collapsible) für request ids.
    2. Contract Registry: update sanitizer für settingsCatalogPolicy

      • In config/graph_contracts.php bei settingsCatalogPolicy ergänzen:
        • entweder update_whitelist (preferred) oder update_strip_keys
      • update_whitelist konservativ starten (nur Felder, die PATCH typischerweise akzeptiert), z. B.:
        • name, description, settings, technologies, platforms, roleScopeTagIds
        • assignments nur wenn Restore wirklich Assignments patcht (sonst weglassen)
      • In GraphContractRegistry (oder äquivalent) Methode bereitstellen:
        • sanitizeUpdatePayload(string $typeKey, array $snapshot): array
        • Entfernt immer: id, createdDateTime, lastModifiedDateTime, @odata.*, version, roleScopeTagIds@odata.*, sowie unbekannte Keys
      • In RestoreService beim UPDATE/PATCH:
        • für settingsCatalogPolicy vor dem Graph PATCH immer sanitizeUpdatePayload() verwenden.
    3. Graph apply: bessere Diagnose im Audit

      • Audit-Event (z. B. restore.item.failed) soll zusätzlich graph_error_code + graph_request_id enthalten (keine Tokens/payloads).

    Tests (Pest)

    • Unit: GraphContractRegistry sanitizer
      • Given snapshot mit read-only/meta Feldern → sanitized payload enthält nur whitelist
    • Feature: Restore execution für settingsCatalogPolicy mit “bad payload”
      • Mock Graph 400 mit error body → RestoreRun result speichert graph_error_message + IDs
      • UI assertion: Fehlermeldung sichtbar (kurz) + Details optional

    Verification

    • ./vendor/bin/pest tests/Unit/GraphContractRegistryTest.php
    • ./vendor/bin/pest tests/Feature/Filament/RestoreExecutionTest.php (oder neues SettingsCatalogPolicyRestoreTest.php)
    • Manuell: RestoreRun detail zeigt bei 400 die echte Graph-Fehlermeldung + request-id; kein generisches “apply failed” ohne Details.

    Acceptance Criteria

    • Restore von settingsCatalogPolicy nutzt PATCH mit sanitiziertem Payload.
    • Bei Fehlern ist im RestoreRun klar ersichtlich warum (Graph error message), inkl. request ids für Support.
  • T178 [US4][Bugfix] Settings Catalog Restore: PATCH strikt auf {name, description, settings} begrenzen + Property-Mapping (displayName→name) + case-insensitive strip

    • Problem:
      • Restore von settingsCatalogPolicy schlägt mit 400 fehl:
        • “Invalid patch, attempting to patch property Platforms is not allowed. Valid properties are Name, Description, and Settings.”
      • Sanitizer lässt platforms/Platforms noch durch und/oder es wird displayName statt name gepatcht.
    • Implementation:
      1. Contract fix (config/graph_contracts.php)
        • Für settingsCatalogPolicy update_whitelist auf exakt:
          • name, description, settings
        • Optional: update_map definieren:
          • displayNamename
          • (und ggf. Description/Settings casing normalisieren)
      2. Sanitizer hardening (app/Services/Graph/GraphContractRegistry.php)
        • Whitelist/Strip case-insensitive anwenden (z. B. Platforms, platforms, PlatformS immer entfernen).
        • Vor dem Final-Payload:
          • Mapping anwenden (displayName→name)
          • Blocklist zusätzlich hart erzwingen: platforms, technologies, templateReference, id, @odata.*, createdDateTime, lastModifiedDateTime
        • Ergebnis-Payload für update muss nur name/description/settings enthalten.
      3. RestoreService (app/Services/Intune/RestoreService.php)
        • Sicherstellen, dass für settingsCatalogPolicy Update-Payload aus Sanitizer kommt (kein “merge back” später).
        • Bei leerem Payload: als noop/skipped behandeln statt PATCH.
    • Tests (Pest):
      • Unit: Sanitizer entfernt platforms/Platforms zuverlässig + mapping displayName→name:
        • tests/Unit/GraphContractRegistryTest.php (erweitern)
      • Feature: Restore Settings Catalog erzeugt PATCH ohne platforms und läuft durch (Graph mocked):
        • tests/Feature/Filament/SettingsCatalogRestoreTest.php (happy-path ergänzen)
    • Verification:
      • ./vendor/bin/pest tests/Unit/GraphContractRegistryTest.php tests/Feature/Filament/SettingsCatalogRestoreTest.php
      • ./vendor/bin/pint --dirty
    • Acceptance:
      • Restore von settingsCatalogPolicy scheitert nicht mehr an Platforms.
      • Results zeigen bei Fehlern weiterhin request-id/client-request-id (bleibt wie T177).
  • T179 [US1b][Scope][settingsCatalogPolicy] Hydrate Settings Catalog “Configuration settings” for all snapshots (versions, backups, previews) and ensure normalized display.

  • Goal: For settingsCatalogPolicy, the Configuration settings (as seen in the Intune Portal under Configuration settings) must be visible throughout the system. This includes:

    • being part of the raw JSON in Policy Versions and Backup Snapshots.
    • being displayed in the Normalized settings section in a readable list/table format.
    • ensuring that Diff, Preview, and Restore operations are based on these detailed settings, not just on general metadata.
  • Why: The base entity for deviceManagement/configurationPolicies often only provides metadata (name, platforms, settingCount, etc.). The actual settings reside in a subresource (e.g., .../configurationPolicies/{id}/settings). Without hydrating this data, TenantPilot cannot display or work with the most relevant policy details like PIN length or biometric settings.


Implementation

1) Centralize Snapshot Hydration

  • In the service responsible for capturing snapshots (e.g., a central PolicySnapshotService, or within VersionService and BackupService), implement a method to hydrate settingsCatalogPolicy data.
  • When the type_key is settingsCatalogPolicy:
    1. GET deviceManagement/configurationPolicies/{id} (Base entity).
    2. GET deviceManagement/configurationPolicies/{id}/settings (with proper paging).
    3. Merge the retrieved settings into the snapshot under a consistent key (e.g., snapshot['settings'] = [...]).
  • This hydration logic MUST be used for creating policy versions, backup items, and restore previews.

2) Enhance PolicyNormalizer

  • In app/Services/Intune/PolicyNormalizer.php, ensure the normalizer can interpret and display the snapshot['settings'] data for settingsCatalogPolicy.
  • It should render a readable table/list of the settings, not just metadata.

3) Update UI Components

  • Ensure the Policy Detail and Policy Version Detail pages use the hydrated snapshots to display the settings.
  • The "Settings available" badge for settingsCatalogPolicy should only show "Available" if the snapshot contains the hydrated settings.

4) Testing

  • Feature Test: Create a SettingsCatalogPolicyHydrationTest.php that:
    • Mocks the Graph API for both the base entity and the /settings subresource.
    • Triggers both a Version Capture and a Backup.
    • Asserts that the resulting PolicyVersion and BackupItem snapshots contain the hydrated settings.
    • Asserts that the Policy Detail and Version Detail pages display the normalized settings correctly.
  • Unit Test: PolicyNormalizerSettingsCatalogTest.php should be updated to verify the rendering of a hydrated snapshot.

Verification

  • ./vendor/bin/pest tests/Feature/Filament/SettingsCatalogPolicyHydrationTest.php

  • ./vendor/bin/pest tests/Unit/PolicyNormalizerSettingsCatalogTest.php

  • ./vendor/bin/pint --dirty

  • T180 - DUPLICATE of T179. Merged into T179.

  • T182 [US1b][settingsCatalogPolicy][UX] Dynamic normalization of Settings Catalog “settings” (generic flatten + readable labels)

  • Goal: settingsCatalogPolicy soll im Normalized settings Bereich nicht mehr nur “setting -” anzeigen, sondern die hydrierten settings[] generisch (ohne hartes Mapping pro Setting) als verständliche Liste/Tabelle darstellen:

    • pro Setting: SettingDefinitionId, Instance Type, Value (und ggf. Choice-Value)
    • nested children / group collections werden rekursiv geflattet
    • optional: einfache Gruppierung (z. B. nach Prefix der definitionId oder “group root”)
  • Why: Microsoft hat unzählige Settings. Wir brauchen eine dynamische Darstellung, die immer funktioniert auch für neue Settings, ohne dass wir jedes Setting kennen.


Implementation

1) PolicyNormalizer: settingsCatalogPolicy → generic flatten

  • In app/Services/Intune/PolicyNormalizer.php:
    • Bei policyType === settingsCatalogPolicy:
      • Wenn snapshot['settings'] existiert:
        • Erzeuge eine Normalizer-Sektion Settings als Tabelle/Repeatable:
          • definitionId (string)
          • instanceType (string, aus settingInstance['@odata.type'])
          • value (string/number/bool/json; aus simpleSettingValue.value oder choiceSettingValue.value)
          • path (optional): zusammengesetzter Pfad zur Einordnung (z. B. parentDefinitionId > childDefinitionId)
        • Implementiere flattenSettingsCatalogSettingInstances(array $settings): array:
          • Iteriere settings[] Einträge
          • Extrahiere settingInstance
          • Unterstütze generisch (mindestens):
            • deviceManagementConfigurationSimpleSettingInstancesimpleSettingValue.value
            • deviceManagementConfigurationChoiceSettingInstancechoiceSettingValue.value
            • deviceManagementConfigurationGroupSettingCollectionInstance:
              • iteriere groupSettingCollectionValue[]
              • rekursiv children[]
            • Fallback: wenn unbekannt → value = json_encode(settingInstance) (kurz/gekürzt)
          • Für Rekursion: maximal Depth (z. B. 8) + Schutz gegen Zyklen/zu große Payloads.
        • Optional: wenn Value ein “enum-like” String ist, zusätzlich displayValue = letzter Token nach _ (nur für bessere Lesbarkeit, ohne Semantik zu behaupten).
      • Wenn settings fehlt:
        • Zeige Banner “Settings not hydrated” (oder “Partial snapshot”) und nur Metadaten.

2) Filament View: bessere Darstellung (Table statt “setting -”)

  • In PolicyResource/ViewPolicy und PolicyVersionResource/ViewPolicyVersion:
    • Stelle sicher, dass die Normalizer-Ausgabe für Settings als Tabelle angezeigt wird:
      • Spalten: Definition, Type, Value, optional Path
    • Lange Values: truncated mit “copy” möglich (oder expand/collapse).

3) Diff: Fokus auf echte Settings (optional, aber empfohlen)

  • In der diff-summary Logik (falls vorhanden):
    • Wenn policyType=settingsCatalogPolicy und settings vorhanden:
      • Summary soll zumindest sagen: “X setting values changed/added/removed”
      • (Die JSON diff bleibt weiterhin verfügbar.)

4) Performance & Safety

  • Guardrails:
    • max rows (z. B. 1000) → danach “truncated”
    • value length max (z. B. 500 chars) → danach “truncated”
    • depth limit
  • Keine Secrets loggen; Normalizer arbeitet nur auf Snapshot JSONB.

Tests (Pest)

Unit: tests/Unit/PolicyNormalizerSettingsCatalogFlattenTest.php

  • Input Snapshot mit:
    • simpleSettingInstance (int)
    • choiceSettingInstance (string)
    • groupSettingCollectionInstance mit children (mix)
  • Assert:
    • Normalizer liefert Settings Sektion mit mehreren Zeilen
    • jede Zeile hat definitionId, instanceType, value
    • rekursive children werden als eigene Zeilen enthalten
    • Unknown instance type fällt auf fallback (json string) ohne crash

Feature: tests/Feature/Filament/SettingsCatalogPolicyNormalizedDisplayTest.php

  • Erzeuge PolicyVersion (settingsCatalogPolicy) mit Snapshot inkl. settings[]
  • Öffne Version-Detail und Policy-Detail
  • Assert:
    • In Normalized settings existiert Sektion “Settings”
    • Tabelle enthält erwartete definitionIds und Werte (z. B. minimumpinlength=12, usebiometrics=true)
    • Keine “setting -” Platzhalter mehr für diesen Snapshot

Regression

  • Bestehende Normalizer-Ausgaben für andere Typen bleiben unverändert.

Verification

  • ./vendor/bin/pest tests/Unit/PolicyNormalizerSettingsCatalogFlattenTest.php
  • ./vendor/bin/pest tests/Feature/Filament/SettingsCatalogPolicyNormalizedDisplayTest.php
  • ./vendor/bin/pint --dirty

Acceptance Criteria

  • Settings Catalog Policy zeigt im Normalized settings Bereich eine verständliche Settings-Tabelle (DefinitionId/Type/Value) statt generischem “setting -”.

  • Rekursive Group/Children-Settings werden sichtbar (nicht verloren).

  • Darstellung ist dynamisch (kein hardcoded mapping pro Setting).

  • Guardrails verhindern UI/Memory Explosion bei sehr großen Policies.

  • T183 [US1b][UX] Make Policy Version detail readable (Tabs + scroll-safe tables)

  • Goal: Policy Version Detail (und optional Policy Detail) soll für Admins lesbar sein:

    • Normalized Settings ist Default/primär sichtbar
    • Raw JSON ist weiterhin verfügbar, aber UI zerbricht nicht durch riesige Payloads
    • Settings Catalog Tabellen/Paths/IDs werden sauber dargestellt (kein “Textsalat”)
  • Why: Aktuell verdrängt Raw JSON + lange SettingDefinitionIds/Paths die gesamte Seite. Admins sehen nicht mehr “was geändert wurde”, sondern nur Datenmüll.


Implementation

1) UI Layout: Tabs (Normalized default, Raw JSON secondary)

  • In app/Filament/Resources/PolicyVersionResource/Pages/ViewPolicyVersion.php (und optional ViewPolicy.php)
    • ersetze die aktuelle Darstellung durch Tabs:
      • Tab 1: Normalized settings (Default)
      • Tab 2: Raw JSON (mit Copy Button)
      • optional Tab 3: Diff (falls vorhanden)
  • Falls Filament Infolist-Komponenten keine Tabs erlauben:
    • nutze eine ViewEntry und rendere Tabs in Blade via x-filament::tabs.

2) Raw JSON: Max height + scroll + monospace

  • In der Raw JSON Blade-View (z.B. resources/views/filament/infolists/entries/raw-json.blade.php oder bestehende View)
    • Wrap <pre> mit:
      • class="max-h-[520px] overflow-auto rounded-lg border bg-gray-50 p-3 text-xs font-mono leading-relaxed"
    • optional: “Expand” action (modal/slideOver) für Vollbildansicht.

3) Normalized settings tables: horizontal scroll + readable columns

  • In resources/views/filament/infolists/entries/normalized-settings.blade.php
    • Table container:
      • class="overflow-x-auto rounded-lg border"
    • Table:
      • class="min-w-[900px] table-fixed"
    • Cells:
      • Definition/Path: font-mono text-xs break-all whitespace-normal
      • Value: break-words whitespace-normal
    • Column widths:
      • Definition: w-[35%], Type: w-[20%], Value: w-[25%], Path: w-[20%]
    • Long values: clamp (optional):
      • line-clamp-2 + “Show more” (details/modal)

4) Optional: Search within Settings (nice-to-have)

  • Add a small client-side filter input (Alpine) above settings table:
    • filters rows by DefinitionId/Value/Path
  • Keep it optional if you want minimal change in v1.

Tests (Pest)

Feature: tests/Feature/Filament/PolicyVersionReadableLayoutTest.php

  • Given a settingsCatalogPolicy version with long settings payload
  • Assert:
    • Tabs render (Normalized + Raw JSON)
    • Raw JSON container has max-height/overflow classes
    • Normalized table wrapper uses overflow-x
    • Page does not contain extremely long unbroken lines without wrappers (basic assertion on classes)

Verification

  • ./vendor/bin/pest tests/Feature/Filament/PolicyVersionReadableLayoutTest.php
  • ./vendor/bin/pint --dirty

Acceptance Criteria

  • Policy Version page is readable on normal screen widths:

    • Normalized settings are immediately visible without scrolling past raw JSON
    • Raw JSON is accessible in second tab and scrolls inside its container
    • Settings table does not break layout; long IDs/paths wrap/scroll cleanly
  • No regressions for other policy types (deviceConfiguration/compliance/scripts).

  • T184 [US1b][UX] Use Filament Tables for Settings Catalog settings (Policy + Version) with responsive layout + SlideOver details

  • Goal: settingsCatalogPolicy Settings sollen lesbar, scannbar und bedienbar sein:

    • als echte Filament Table (nicht “pseudo table” im Infolist-Blade)
    • mit truncate + tooltip, horizontal scroll, sticky header
    • mit Details (SlideOver) + Copy pro Row
    • identisch nutzbar in Policy Detail und Policy Version Detail
  • Why: Die aktuelle Darstellung bricht Layout/Spaltenbreiten (Definition/Type/Value laufen ineinander). Filament Tables lösen genau diese Probleme (fixed layout, responsive, actions, search).


Implementation

1) Introduce reusable Livewire component for Settings Catalog settings table

  • New: app/Livewire/SettingsCatalogSettingsTable.php
    • Props:
      • array $settingsRows (aus PolicyNormalizer Output oder direkt aus Snapshot settings)
      • string $context (policy|version) optional
    • Intern: build a Filament Table with columns:
      • Definition (TextColumn::make('definition'))
        • wrap(false), searchable(), tooltip(fn($state) => $state), limit(60)
      • Type (TextColumn::make('type'))
        • wrap(false), toggleable(), limit(50), tooltip(...)
      • Value (TextColumn::make('value'))
        • wrap(false), limit(60), tooltip(...)
        • render (group) badge for group rows
      • Path (TextColumn::make('path'))
        • toggleable(isToggledHiddenByDefault: true), limit(80), tooltip(...)
    • Table config:
      • paginated([25, 50, 100]) (default 25)
      • searchPlaceholder('Search definition/value…')
      • striped(), deferLoading()
    • Row Action:
      • Action::make('details')->label('Details')->icon('heroicon-m-eye')
      • opens SlideOver
        • shows full Definition/Type/Value/Path + optional raw setting JSON (pretty)
        • Copy buttons for Definition/Value

2) Embed component via ViewEntry in Policy + PolicyVersion detail

  • Policy detail (app/Filament/Resources/PolicyResource/Pages/ViewPolicy.php)
    • For settingsCatalogPolicy:
      • render SettingsCatalogSettingsTable (instead of current table block)
      • pass rows from Normalizer (normalize() should expose a stable rows array)
  • PolicyVersion detail (app/Filament/Resources/PolicyVersionResource/Pages/ViewPolicyVersion.php)
    • same embedding for settingsCatalogPolicy

Rule: Nur für settingsCatalogPolicy auf Table UI umstellen. Andere Typen bleiben Infolist/KeyValue.

3) Tailwind/Filament styling guardrails (no layout break)

  • Ensure table container is responsive:
    • wrap table in div class="overflow-x-auto"
    • set columns non-wrapping by default (truncate)
  • Sticky header:
    • enable sticky header in table (Filament supports sticky header via table wrapper CSS; if needed add a small CSS utility class in your Filament theme)

4) Normalizer output contract (stable)

  • Ensure PolicyNormalizer returns for settingsCatalogPolicy:
    • ['settings_table' => ['columns' => [...], 'rows' => [...]]]
    • rows fields: definition, type, value, path, raw (optional)
  • Table uses rows, not parsing raw snapshot again (single source).

Tests (Pest)

Feature: tests/Feature/Filament/SettingsCatalogSettingsTableRenderTest.php (new)

  • Create a policy + version with hydrated settings
  • Visit Policy detail and PolicyVersion detail
  • Assert:
    • table headers visible (Definition/Type/Value)
    • at least one known definition appears
    • “Details” action exists

Unit (optional): tests/Unit/PolicyNormalizerSettingsCatalogRowsTest.php

  • Given snapshot with nested settings instances
  • Assert normalizer returns rows with definition/type/value/path

Verification

  • ./vendor/bin/pest tests/Feature/Filament/SettingsCatalogSettingsTableRenderTest.php
  • ./vendor/bin/pest tests/Unit/PolicyNormalizerSettingsCatalogRowsTest.php
  • ./vendor/bin/pint --dirty

Acceptance Criteria

  • In Policy Detail und Policy Version Detail sind Settings Catalog Settings als Filament Table sichtbar (lesbar, nicht überlappend).

  • Lange Werte sind truncated aber per Tooltip/Details vollständig erreichbar.

  • Pro Row gibt es Details SlideOver + Copy.

  • Kein Layout-Bruch auf typischen Screenbreiten (Laptop/FullHD).

  • T185 [UX][US1b][settingsCatalogPolicy] Make Settings Catalog settings readable (label/value parsing + table ergonomics)

  • Goal: Settings Catalog Policies sollen im Policy/Version Detail für Admins lesbar sein, ohne dass wir “alle Settings kennen müssen”.

    • Tabelle zeigt sprechende Bezeichnung + kompakte Werte
    • Lange IDs bleiben verfügbar (Tooltip/Copy/Details), aber dominieren nicht die UI
  • Why: Aktuell sind definitionId und choiceSettingValue.value so lang, dass sie in der Tabelle abgeschnitten werden und der Admin weder Setting noch Wert versteht.


Implementation

1) Presentation layer: generate human-friendly labels (no registry needed)

  • Add helper in PolicyNormalizer (oder kleiner SettingsCatalogPresenter):
    • labelFromDefinitionId(string $definitionId): string
      • remove common prefixes: device_vendor_msft_, user_vendor_msft_, policy_config_, admx_
      • replace _ with spaces
      • keep last 24 segments if string is huge
      • replace {tenantid} with {tenant}
    • Output example:
      • user_vendor_msft_policy_config_admx_desktop_nomydocumentsico...Desktop: No My Documents Icon (heuristic), fallback: last segments nicely spaced

Heuristic only. If no good split possible, fallback to “last segments” label.

2) Parse values into a short “effective value”

  • Implement valuePreview(array $settingInstance): string:
    • For SimpleSettingValue: return scalar (12, 0, true/false)
    • For ChoiceSettingValue.value: return last token after last _ OR map known boolean patterns:
      • suffix _true/_falseTrue/False
      • suffix _0/_1 for allowed/blocked → show 0/1 but also tag Allowed/Blocked if detectable
    • For group instances: show (group) and put children into details view only

3) Improve table ergonomics (Filament Table / Livewire)

  • In SettingsCatalogSettingsTable:
    • Columns:
      1. Setting (human label) + small muted secondary line showing truncated definitionId
      2. Value (valuePreview)
      3. Type (badge: Choice/Simple/Group)
      4. Optional: Path (toggleable, hidden by default)
    • Add:
      • ->searchable() should search both label + raw definitionId + raw value
      • ->wrap() / ->limit() for long strings
      • tooltips showing full definitionId/value on hover
      • “Copy” icon action in row details (SlideOver) for Definition + Raw JSON
    • Ensure horizontal scroll only inside table container:
      • wrapper div with overflow-x-auto + max-w-full
      • table layout fixed where possible (table-fixed) to prevent column blowouts

4) Keep Raw JSON accessible but not primary

  • In PolicyVersion view:
    • Put Raw JSON into collapsible section or separate tab.
    • Normalized Settings tab becomes default for settingsCatalogPolicy.

Tests (Pest)

Unit: tests/Unit/SettingsCatalogPresenterTest.php

  • labelFromDefinitionId() produces readable output and stable fallback
  • valuePreview() returns expected previews for:
    • choice true/false, numeric, group

Feature: tests/Feature/Filament/SettingsCatalogSettingsTableUsabilityTest.php

  • Render policy detail with one very long definition + long choice value
  • Assert:
    • label column shows shortened readable label (not the full raw string)
    • value column shows preview (e.g., True, 12, Never)
    • details slide-over contains full raw definition/value + copy UI

Verification

  • ./vendor/bin/pest tests/Unit/SettingsCatalogPresenterTest.php tests/Feature/Filament/SettingsCatalogSettingsTableUsabilityTest.php
  • ./vendor/bin/pint --dirty

Acceptance Criteria

  • In Policy Detail, Settings table shows:
    • Readable Setting name (not a cut-off vendor string)
    • Readable Value preview (True/False/12/etc.)
  • Full raw definitionId and raw value remain accessible via tooltip and SlideOver + copy button.
  • No layout overflow/broken columns on common laptop viewport widths.

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).
    • Implements: FR-001
  • T010 [US1] Create Filament resource/table for policies with filters and metadata columns in app/Filament/Resources/PolicyResource.php.
    • Implements: FR-001
  • 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.

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

    • Implements: FR-002
  • 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.

    • Implements: FR-003

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.
    • Implements: FR-004
  • 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.
    • Implements: FR-005
  • 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.
    • Implements: FR-022
  • T025 [US4] Record restore run lifecycle (start, per-item result, completion) and audit events in restore_runs and audit_logs.
    • Implements: FR-007
  • T156 [US4][UX] Add “Rerun” action to RestoreRun row actions (ActionGroup): creates a new RestoreRun cloned from selected run (same backup_set_id, same selected items, same dry_run flag), enforces same safety gates/confirmations as original execution path, writes audit event restore_run.rerun_created with source_restore_run_id.
    • Implements: FR-035
    • Implements: FR-035

Phase 7: User Story 5 - Operational readiness and environments (Priority: P2)

  • 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.
    • Implements: FR-010
    • Implements: FR-011
    • Implements: FR-012
  • 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: Tenant Management (Tenant hinzufügen, App-Setup, Verify) (Priority: P1)

Hinweis: Diese Phase ist “Tenant Management” und nicht US6, damit US6 sauber “Permissions/Health” bleibt.

  • T030 [TENANT] Migration für tenants ergänzen/prüfen (name, tenant_id, domain, app_client_id, app_status, app_notes, timestamps).
    • Implements: FR-011
    • Implements: FR-014
    • Implements: FR-015
    • Implements: FR-016
    • Implements: FR-017
    • Implements: FR-018
  • T031 [TENANT] Eloquent Model Tenant (Beziehungen, tenant-aware scopes).
  • T032 [TENANT] Filament TenantResource (list/create/edit/detail; Actions: Open in Entra, Copy consent URL optional).
  • T033 [TENANT] TenantConfigService / Graph connectivity check.
  • T034 [TENANT] Action „Verify configuration“ + Audit (tenant.config.verified).
  • T035 [TENANT] Tenant-Kontext in Policy/Backup/Restore/Audit Services (tenant_id überall setzen).
    • Implements: FR-009
  • T036 [TENANT] Feature-Test TenantSetupTest (ok/error + Audit).
  • T037 [TENANT] Admin-Consent Callback Route (state → tenant mapping, status update, UI page).

Phase 9: User Story 6 - Berechtigungsübersicht & Health-Status (Priority: P1)

  • T040 [P] [US6] Zentrale Permissions-Liste config/intune_permissions.php (+ optional docs/permissions.md).
    • Implements: FR-006
    • Implements: FR-008
  • T041 [US6] Datenmodell Tenant-Berechtigungen (JSONB granted_permissions oder tenant_permissions Tabelle; status ok/missing/error).
  • T042 [US6] TenantPermissionService (required, granted, compare DTO).
  • T043 [US6] Tenant-Detail UI Panel „Permissions“ (required list + status).
  • T044 [US6] Verify erweitern: compare + persist + Audit tenant.permissions.checked.
    • Implements: FR-006
    • Implements: FR-008
  • T045 [US6] Tests: Unit compare + Feature Tenant detail status + Verify updates.

Phase 9b: Scope-Ausrichtung auf neue Objekttypen

  • T028 [Scope] config/tenantpilot.php auf scope.supported_types erweitern; single source for sync/backup/restore.
  • T029 [Scope] Filament-UI an neue Typen anpassen (Filter/Grouping + Restore-Level Hinweise).

Phase 10: Housekeeping Delete-Funktionen für Backups & Versions

  • T060 [HK] BackupSets soft deletable + Guard gegen RestoreRuns.
  • T061 [HK] Filament Delete BackupSets + Confirmation + Audit (backup.deleted) + Guard.
  • T062 [HK] PolicyVersions soft deletable + Queries/Resources default non-deleted.
  • T063 [HK] Filament Delete PolicyVersions + Audit (policy_version.deleted).
  • T064 [HK] Tests Housekeeping (BackupSet delete ok/block + PolicyVersion delete).

Phase 11: Housekeeping Tenant löschen/deaktivieren

  • T070 [HK] Tenants soft deletable (optional status active/archived).
  • T071 [HK] Tenant deactivate/archive action + Audit (tenant.archived).
  • T072 [HK] Block operations for deactivated tenants (sync/backup/restore early fail).
  • T073 [HK] RestoreRuns soft deletable (optional) + Audit (restore_run.deleted).
  • T074 [HK] Tests Tenant delete/deactivate behavior + clear errors, no Graph calls.

Phase 12: Housekeeping Hard Deletes (Force Delete)

  • T075 [HK] Force-Delete-Actions (only in trashed; guards; audit before delete) + tests.

Phase 12b: Single current tenant ("Highlander")

  • T120 [TENANT] Migration add is_current + partial unique index.

  • T121 [TENANT] Tenant::current() + makeCurrent() + remove implicit defaults.

  • T122 [TENANT] Data cleanup (mark one current; archive local-tenant).

  • T123 [TENANT] Filament UI badge + “Make current” action.

  • T124 [TENANT] Consumers refactor to Tenant::current() or explicit tenant.

  • T125 [TENANT] Tests for current selection + “Make current”.

  • T130 [UX] Tabellen-Aktionen in Dropdown bündeln (ActionGroup) in TenantResource (+ optional others).

Phase 13: Settings Normalization & Display (Priority: P1)

  • T140 [P] [US1b] Unit test PolicyNormalizer.

  • T141 [P] [US1b] Feature test Policy Settings section.

  • T142 [P] [US1b] Feature test Version detail pretty JSON + normalized.

  • T143 [P] [Edge] Feature test malformed snapshot warning.

  • T144 [P] [Edge] Feature test @odata.type mismatch flag + restore exec block.

  • T145 [US1b] PolicyNormalizer service.

    • Implements: FR-019
  • T146 [US1b] Settings infolist in PolicyResource.

  • T147 [US1b] PolicyVersion view pretty JSON + normalized.

  • T148 [US1b] Integrations (list badge, optional diff enhancements, tenant scoping).

  • T149 [Edge] SnapshotValidator helper.

  • T150 [Edge] @odata.type validator (policy/backup/restore gates).

  • T151 [Edge] UI warnings + restore execution gating (preview may show).

  • T152 [US1b] README docs for settings display.

  • T153 [US1b] Inline docs in PolicyNormalizer.

Phase 14: User Story 7 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 [US7] Add TenantResource ActionGroup entry “Setup Intune RBAC”: visible for active tenants with app_client_id; sits alongside Admin consent/Verify; guarded by Highlander rules. Verified by: ./vendor/bin/pest tests/Feature/Filament/TenantRbacWizardTest.php.

    • Implements: FR-023
    • Implements: FR-025
    • Implements: FR-026
    • Implements: FR-027
    • Implements: FR-028
    • Implements: FR-029
  • T161 [US7] Wizard UI (Filament): Step 1 preconditions/summary with inputs for Role, Scope, Group mode; least-privilege warnings; review screen of planned changes. Verified by: ./vendor/bin/pest tests/Feature/Filament/TenantRbacWizardTest.php.

  • T162 [US7] Delegated auth step: initiate delegated login; stop with clear error + audit on failure; token not persisted. Verified by: ./vendor/bin/pest tests/Feature/Filament/TenantRbacWizardTest.php.

  • T163 [US7] Execution service (sync) with audit per step: resolve SP by app_client_id; ensure/create security group (securityEnabled=true); add SP as member (idempotent); ensure/create/update Intune role assignment; persist IDs on tenant for idempotency. Verified by: ./vendor/bin/pest tests/Feature/Filament/TenantRbacWizardTest.php.

    • Implements: FR-030
  • T164 [US7] Post-check (mandatory): clear app token cache / force fresh token acquisition and run canary reads:

    • GET /deviceManagement/deviceConfigurations?$top=1
    • GET /deviceManagement/deviceCompliancePolicies?$top=1
    • optional CA canary only if CA features enabled
    • update tenant health + audit verify outcome. Verified by: ./vendor/bin/pest tests/Feature/Filament/TenantRbacWizardTest.php.
  • T165 [US7] Tests (Pest, mocked Graph): happy path; rerun idempotent; missing permissions error mapping; scope-limited warning; delegated login failure path; non-security-enabled group failure. Verified by: ./vendor/bin/pest tests/Feature/Filament/TenantRbacWizardTest.php.

  • T166 [US7] Documentation: README note for wizard behavior (delegated, sync), least-privilege defaults, audit expectations, rerun safety. Verified by: manual review of README.md update.

  • T167 [US7-Optional] CLI/Job for CHECK/REPORT only (no grant), explicitly exclude async grant.

  • T168 [US7] Extend Verify configuration / Health panel to include “Intune RBAC status” (OK/Missing/Error) + CTA “Run Setup Intune RBAC”, persist last_checked_at + reason; Audit tenant.rbac.checked. Verified by: ./vendor/bin/pest tests/Feature/Filament/TenantRbacWizardTest.php.

    • Implements: FR-024
  • T169 [US7] Persist RBAC artifacts on Tenant for idempotency:

    • migration add nullable columns: rbac_group_id, rbac_group_name, rbac_role_assignment_id, rbac_role_key, rbac_scope_mode, rbac_scope_id
    • prefer stored IDs on reruns; discovery fallback. Verified by: ./vendor/bin/pest tests/Feature/Filament/TenantRbacWizardTest.php.

Phase 15: User Story 8 Graph Contract Registry & Drift Guard

Scope: FR-031 to FR-034; contract registry per type, type-family handling, capability fallbacks, drift checks.

  • T170 [US8] Add contract registry artifact (e.g., config/graph_contracts.php) capturing per supported type: resource paths, allowed $select/$expand, allowed @odata.type family, create/update methods, id field, hydration strategy. Verified by: manual review.
  • T171 [US8] Implement registry service + integration in Graph client to enforce allowed capabilities and downgrade on capability errors (retry without expand/select), logging warnings/audit entries. Verified by: ./vendor/bin/pest tests/Unit/GraphContractFallbackTest.php.
    • Implements: FR-032
  • T172 [US8] Implement type-family handling so derived @odata.type within a family routes correctly for preview/restore (no odata_mismatch) while still blocking unknown types. Verified by: ./vendor/bin/pest tests/Feature/Filament/ODataTypeMismatchTest.php.
    • Implements: FR-033
  • T173 [US8] Add verification command php artisan graph:contract:check (staging/CI) to probe endpoints, detect drift, and emit actionable diff/log output; make prod opt-in/guarded. Verified by: manual review.
    • Implements: FR-034
  • T174 [US8] Tests (Pest/unit/integration): registry lookups, fallback selection on capability errors, derived type acceptance, drift-check command behavior. Verified by: ./vendor/bin/pest tests/Unit/GraphContractRegistryTest.php tests/Unit/GraphContractFallbackTest.php.
  • T175 [US8] Documentation: describe registry format/update process, fallback behavior, and how/when to run graph:contract:check. Verified by: manual review of README update.