From de7e7dea57bb0d3440984bec63d8db1be9e6cea9 Mon Sep 17 00:00:00 2001 From: Ahmed Darrazi Date: Sat, 10 Jan 2026 22:30:37 +0100 Subject: [PATCH] spec: archive legacy specs under 000-initial-config --- specs/000-initial-config/plan.md | 94 +++ specs/000-initial-config/research_t186.md | 50 ++ specs/000-initial-config/spec.md | 835 ++++++++++++++++++++ specs/000-initial-config/tasks.md | 912 ++++++++++++++++++++++ 4 files changed, 1891 insertions(+) create mode 100644 specs/000-initial-config/plan.md create mode 100644 specs/000-initial-config/research_t186.md create mode 100644 specs/000-initial-config/spec.md create mode 100644 specs/000-initial-config/tasks.md diff --git a/specs/000-initial-config/plan.md b/specs/000-initial-config/plan.md new file mode 100644 index 0000000..a700cf4 --- /dev/null +++ b/specs/000-initial-config/plan.md @@ -0,0 +1,94 @@ +# Implementation Plan: TenantPilot v1 (ARCHIVED) + +> ARCHIVED / legacy copy. +> Do not use this folder for active work. +> Active feature plans live under `specs/-/` on `feat/-` branches. + +**Original Date**: 2026-01-03 +**Archived Copy Updated**: 2026-01-10 +**Spec Source (legacy)**: previous `.specify/*` artifacts (scope/restore matrix is config-driven) + +## Summary +TenantPilot v1 delivers tenant-scoped Intune inventory, immutable backups, version history with diffs, defensive restore flows, tenant setup, permissions/health, settings normalization/display, Highlander enforcement, the delegated RBAC onboarding wizard (US7), and the Graph Contract Registry & Drift Guard (US8). All Graph calls stay behind the abstraction with audit logging; snapshots remain JSONB with safety gates (preview-only for high-risk types). + +## Status Snapshot (tasks.md is source of truth) +- **Done**: Phases 1–15 (US1–US8, Settings Catalog hydration/display, restore rerun, Highlander, permissions/health, housekeeping/UX, ops). +- **Open**: T167 (optional) CLI/Job for CHECK/REPORT only (no grant). +- **Next up**: Feature 018 (Driver Updates / WUfB add-on) in `specs/018-driver-updates-wufb/`. + +## Technical Baseline +- Laravel 12, Filament 4, PHP 8.4; Sail-first with PostgreSQL. +- JSONB for policy/backup/version payloads; FK/time indexes, GIN where needed. +- Graph abstraction with standardized error mapping/retries; no secrets in logs. +- Audit trail across backup/restore/version/tenant/permission/wizard steps; tenant isolation enforced. +- Restore matrix and supported types remain config-driven single sources of truth. +- Safety: preview/dry-run, confirmation gates, warnings for high-risk types; no implicit tenants (Highlander). + +## Completed Workstreams (no new action needed) +- **US1 Inventory (Phase 3)**: Filament policy listing with type/category/platform filters; tenant-scoped. +- **US2 Backups (Phase 4)**: Backup sets/items in JSONB, immutable snapshots, audit logging, relation manager UX for attaching policies, soft-delete rules with restore-run guard. +- **US3 Versions/Diffs (Phase 5)**: Version capture, timelines, human+JSON diffs, soft-deletes with audit. +- **US4 Restore (Phase 6)**: Preview, selective execution, conflict warnings, per-type restore level (enabled vs preview-only), PowerShell decode/encode respected, audit of outcomes; settings catalog fallback creates a new policy when the settings endpoint is unsupported, retrying metadata-only creation if settings are not accepted, recording the new policy id and manual warnings. +- **US6 Tenant Setup & Highlander (Phases 8 & 12)**: Tenant CRUD/verify, INTUNE_TENANT_ID override, `is_current` unique enforcement, “Make current” action, block deactivated tenants. +- **US6 Permissions/Health (Phase 9)**: Required permissions list, compare/check service, Verify action updates status and audit, permissions panel in Tenant detail. +- **US1b Settings Display (Phase 13)**: PolicyNormalizer + SnapshotValidator, warnings for malformed snapshots, normalized settings and pretty JSON on policy/version detail, list badges, README section. +- **US7 RBAC Wizard (Phase 14)**: Delegated, synchronous onboarding wizard with post-verify canary checks and audit trail. +- **US8 Graph Contracts & Drift Guard (Phase 15)**: Config-driven contract registry, type-family handling, capability downgrade fallbacks, and a drift-check command. +- **Housekeeping/UX (Phases 10–12)**: Soft/force deletes for tenants/backups/versions/restore runs with guards; table actions in ActionGroup per UX guideline. +- **Ops (Phase 7)**: Sail runbook and Dokploy staging→prod guidance captured. + +## Completed: US7 Intune RBAC Onboarding Wizard (Phase 14) + +- Objectives: deliver delegated, tenant-scoped wizard that safely converges the Intune RBAC state for the configured service principal; fully audited, idempotent, least-privilege by default. +- Scope alignment: FR-023–FR-030, constitution (Safety-First, Auditability, Tenant-Aware, Graph Abstraction). No secret/token persistence; delegated tokens stay request-local and are not stored in DB/cache. +- Design decisions: + - Service: `RbacOnboardingService` orchestrates steps using `GraphClientInterface`; reuse `RbacHealthService` for verification; all calls through abstraction with error mapping. +- Data: use existing tenant RBAC columns (`rbac_group_id`, `rbac_group_name`, `rbac_role_assignment_id`, `rbac_role_key`, `rbac_scope_mode`, `rbac_scope_id`, status fields). No new entities; ensure casts + guards. +- Audit: log start, delegated login outcome, group ensure, membership ensure, role assignment ensure/update, verify results. No payload logging; only IDs/status codes. +- Wizard flow (Filament, Tenant detail ActionGroup): + 1) Preconditions/config step with review screen: show tenant/app info, required permissions, least-privilege warning; inputs for role (default Policy/Profile Manager; Intune Administrator shows warning), scope (global default; optional group picker), group mode (create default `TenantPilot-Intune-RBAC` vs pick existing security-enabled group). Summarize planned changes before proceeding. + 2) Delegated auth step: initiate login; on failure stop with actionable message + audit; do not store token beyond request. + 3) Execute (synchronous): resolve service principal by `app_client_id`; on missing SP stop with consent-required hint + audit reason `sp_not_found`; ensure/create security group (validate `securityEnabled=true`); ensure SP membership (idempotent “already exists” OK); ensure/create/patch Intune role assignment for chosen role/scope; persist discovered IDs on tenant for idempotency. + 4) Post-verify: force fresh token acquisition; run canary reads (deviceConfigurations, deviceCompliancePolicies, conditionalAccess if enabled); update RBAC/permission health; surface warnings if scope-limited; audit verify result. + 5) Summary: show IDs (group, role assignment), role/scope used, verify status, CTA to retry policy sync. +- UX rules: action only for active tenants with `app_client_id`; keep in ActionGroup with Admin consent/Verify; show badge/hint if RBAC missing; warnings on selecting Intune Administrator role; block execution if tenant inactive or missing consent/SP. +- Safety/idempotency: handle “already exists” as success; no self-heal jobs; retry-safe writes; no queue usage to avoid token expiry; timeouts surfaced clearly; no delegated token persistence. +- Tests: happy path, rerun idempotent, SP missing, insufficient privileges, non-security-enabled group failure, scope-limited warning, delegated auth failure path; Filament wizard visibility + summary rendering; health prompts to run wizard when RBAC missing. +- Documentation: add wizard behavior, least-privilege defaults, audit expectations, “no token storage”, and how to rerun safely; note CTA to retry policy sync. +- Operational note: After admin-consent or RBAC changes, force a fresh token acquisition (e.g., clear app token cache) before re-trying sync/backup/restore; Verify should run with a non-stale token. Optional CHECK/REPORT jobs only (no grant) remain out-of-scope for this phase. +- Testing plan (Pest): + - Service unit tests: happy path, rerun idempotent, SP missing, insufficient privileges, scope-limited warning, group exists/not security-enabled failure. + - Filament feature: wizard visibility gating, delegated failure path, successful run shows summary and updates health, warnings rendered. + - Health integration: Verify reflects RBAC status and prompts to run wizard when missing. +- Deployment/ops: no new env vars; ensure migrations for tenant RBAC columns are applied; run targeted tests `php artisan test tests/Unit/RbacOnboardingServiceTest.php tests/Feature/Filament/TenantRbacWizardTest.php`; Pint on touched files. + +## Completed: US8 Graph Contract Registry & Drift Guard (Phase 15) + +- Objectives: centralize Graph contract assumptions per supported type/endpoint and provide drift detection + safe fallbacks so preview/restore remain stable on Graph shape/capability changes. +- Scope alignment: FR-031–FR-034 (spec), constitution (Safety-First, Auditability, Graph Abstraction, Tenant-Aware). +- Approach: + - Artifact: `config/graph_contracts.php` (or similar) with per-type contract data: + - resource paths (collection + single item) + - allowed `$select` / allowed `$expand` + - **type families / allowed `@odata.type` values** + - create/update methods, id field + - hydration strategy (member expansion vs follow-up fetch vs unavailable) + - Service: registry + checker; integrate with Graph client to enforce allowed capabilities and downgrade on capability errors (retry without expands/selects), recording warnings/audit entries. + - Type families: treat derived `@odata.type` values **within a declared family** as compatible (no `odata_mismatch`) for routing preview/restore. + - Verification: `php artisan graph:contract:check` (staging/CI) to probe endpoints and surface actionable diffs when Graph changes; opt-in/guarded for prod. + - Docs: explain registry format and update process when Graph changes. +- Testing outline: unit for registry lookups/type-family matching/fallback selection; integration/Pest to simulate capability errors and ensure downgrade path + correct routing for derived types. + +## Testing & Quality Gates +- Continue using targeted Pest runs per change set; add/extend tests when RBAC/contract behavior changes. +- Run Pint on touched files before finalizing. +- Maintain tenant isolation, audit logging, and restore safety gates; validate snapshot shape and type-family compatibility prior to restore execution. + +### Restore Safety Gate +- Restore execution MUST be blocked if a snapshot’s `@odata.type` is **outside** the declared **type family** for the target policy type (prevent cross-type/platform restores). +- Restore preview MAY still render details + warnings for out-of-family snapshots, but MUST NOT offer an apply action. + +## Coordination +- Keep `.specify/tasks.md` and per-feature specs under `specs/` aligned with implementation changes. +- Stage validation required before production for any migration or restore-impacting change. +- Keep Graph integration behind abstraction; no secrets in logs; follow existing UX patterns (ActionGroup, warnings for risky ops). diff --git a/specs/000-initial-config/research_t186.md b/specs/000-initial-config/research_t186.md new file mode 100644 index 0000000..86215ef --- /dev/null +++ b/specs/000-initial-config/research_t186.md @@ -0,0 +1,50 @@ +# Research T186 — settings_apply capability verification (ARCHIVED) + +> ARCHIVED / legacy copy. +> Do not use this folder for active work. +> Active feature research notes should live under `specs/-/` on `feat/-` branches. + +Objective +--------- +Verify whether the Microsoft Graph endpoint `deviceManagement/configurationPolicies/{id}/settings` accepts writes (POST/PUT) for applying Settings Catalog settings and document the exact request body shape and required `@odata.type` values. + +Context +------- +Logs show `PATCH` to the parent resource fails with `ModelValidationFailure: Cannot apply PATCH to navigation property 'settings'`. A fallback implemented in RestoreService attempts to `POST` to `.../{id}/settings` but tenant behavior is inconsistent (some tenants return `NotSupported`). + +Verification Steps +------------------ +1. Choose a test tenant and service principal that reflect the production app permissions. +2. Fetch a sample Settings Catalog policy: + +```http +GET /deviceManagement/configurationPolicies/{id} +``` + +3. Fetch settings subresource: + +```http +GET /deviceManagement/configurationPolicies/{id}/settings +``` + +4. Construct a minimal settings payload (single setting) including `settingInstance.@odata.type` and try POST: + +```http +POST /deviceManagement/configurationPolicies/{id}/settings +Content-Type: application/json + +[ { } ] +``` + +5. If POST fails, record full response body and headers (request-id, client-request-id). Try alternative shapes (e.g. POST `{ "settings": [...] }`) and different methods (PUT) if documented. + +6. Capture any success responses and validate resulting settings in the portal or via subsequent GET. + +Deliverables +------------ +- `research_t186.md` (this file) populated with observed request/response bodies and decision (A: supported — include exact body_shape; B: unsupported — document fallback and admin instructions). +- If supported, proposed `config/graph_contracts.php` entry finalized and tests updated. + +Notes +----- +- Do not include secrets in this document. Paste only non-sensitive request/response metadata and request ids. diff --git a/specs/000-initial-config/spec.md b/specs/000-initial-config/spec.md new file mode 100644 index 0000000..6f1ab08 --- /dev/null +++ b/specs/000-initial-config/spec.md @@ -0,0 +1,835 @@ +# Feature Specification: TenantPilot v1 (ARCHIVED) + +> ARCHIVED / legacy copy. +> Do not use this folder for active work. +> Active feature specs live under `specs/-/` on `feat/-` branches. + +**Created**: 2025-12-10 +**Archived Copy Updated**: 2026-01-10 +**Status**: Archived +**Input**: TenantPilot v1 scope covering Intune configuration inventory (config, compliance, scripts, apps, conditional access, endpoint security, enrollment/autopilot, RBAC), backup, version history, and defensive restore for Intune administrators. + +## Scope + +```yaml +scope: + description: "v1 muss folgende Intune-Objekttypen inventarisieren, sichern und – je nach Risikoklasse – wiederherstellen können. Single Source of Truth: config/tenantpilot.php + config/graph_contracts.php." + supported_types: + - key: deviceConfiguration + name: "Device Configuration" + graph_resource: "deviceManagement/deviceConfigurations" + filter: "not isof('microsoft.graph.windowsUpdateForBusinessConfiguration')" + notes: "Standard Device Config inkl. Custom OMA-URI; excludes WUfB Update Rings." + + - key: groupPolicyConfiguration + name: "Administrative Templates" + graph_resource: "deviceManagement/groupPolicyConfigurations" + notes: "Administrative Templates (Group Policy)." + + - key: settingsCatalogPolicy + name: "Settings Catalog Policy" + graph_resource: "deviceManagement/configurationPolicies" + notes: "Settings Catalog policies; settings are hydrated from the /settings subresource." + + - key: windowsUpdateRing + name: "Software Update Ring" + graph_resource: "deviceManagement/deviceConfigurations" + filter: "isof('microsoft.graph.windowsUpdateForBusinessConfiguration')" + notes: "Windows Update for Business (WUfB) update rings." + + - key: windowsFeatureUpdateProfile + name: "Feature Updates (Windows)" + graph_resource: "deviceManagement/windowsFeatureUpdateProfiles" + + - key: windowsQualityUpdateProfile + name: "Quality Updates (Windows)" + graph_resource: "deviceManagement/windowsQualityUpdateProfiles" + + - key: windowsDriverUpdateProfile + name: "Driver Updates (Windows)" + graph_resource: "deviceManagement/windowsDriverUpdateProfiles" + + - key: deviceCompliancePolicy + name: "Device Compliance" + graph_resource: "deviceManagement/deviceCompliancePolicies" + + - key: appProtectionPolicy + name: "App Protection (MAM)" + graph_resource: "deviceAppManagement/managedAppPolicies" + notes: "iOS und Android Managed App Protection." + + - key: mamAppConfiguration + name: "App Configuration (MAM)" + graph_resource: "deviceAppManagement/targetedManagedAppConfigurations" + notes: "App configuration targeting managed apps (MAM)." + + - key: managedDeviceAppConfiguration + name: "App Configuration (Device)" + graph_resource: "deviceAppManagement/mobileAppConfigurations" + notes: "Managed device app configuration profiles." + + - key: conditionalAccessPolicy + name: "Conditional Access" + graph_resource: "identity/conditionalAccess/policies" + notes: "Kritisch für Sicherheit. Policy.Read.All/Policy.ReadWrite.All nötig; v1: Restore nur mit starker Preview." + + - key: deviceManagementScript + name: "PowerShell Scripts" + graph_resource: "deviceManagement/deviceManagementScripts" + notes: "scriptContent wird beim Backup base64-decoded gespeichert und beim Restore wieder encoded (vgl. FR-020)." + + - key: deviceShellScript + name: "macOS Shell Scripts" + graph_resource: "deviceManagement/deviceShellScripts" + + - key: deviceHealthScript + name: "Proactive Remediations" + graph_resource: "deviceManagement/deviceHealthScripts" + + - key: enrollmentRestriction + name: "Enrollment Restrictions" + graph_resource: "deviceManagement/deviceEnrollmentConfigurations" + + - key: windowsAutopilotDeploymentProfile + name: "Windows Autopilot Profiles" + graph_resource: "deviceManagement/windowsAutopilotDeploymentProfiles" + + - key: windowsEnrollmentStatusPage + name: "Enrollment Status Page (ESP)" + graph_resource: "deviceManagement/deviceEnrollmentConfigurations" + notes: "Filtered to #microsoft.graph.windows10EnrollmentCompletionPageConfiguration." + + - key: endpointSecurityIntent + name: "Endpoint Security Intents" + graph_resource: "deviceManagement/intents" + notes: "Account Protection, Disk Encryption etc.; Zuordnung über bekannte Templates." + + - key: endpointSecurityPolicy + name: "Endpoint Security Policies" + graph_resource: "deviceManagement/configurationPolicies" + notes: "Configuration policies classified via technologies/templateReference; restore execution enabled with template validation (Feature 023)." + + - key: securityBaselinePolicy + name: "Security Baselines" + graph_resource: "deviceManagement/configurationPolicies" + notes: "High risk; v1 restore stays preview-only." + + - key: mobileApp + name: "Applications (Metadata only)" + graph_resource: "deviceAppManagement/mobileApps" + notes: "Backup nur von Metadaten/Zuweisungen (kein Binary-Download in v1)." + + foundation_types: + - key: assignmentFilter + name: "Assignment Filter" + graph_resource: "deviceManagement/assignmentFilters" + + - key: roleScopeTag + name: "Scope Tag" + graph_resource: "deviceManagement/roleScopeTags" + + - key: notificationMessageTemplate + name: "Notification Message Template" + graph_resource: "deviceManagement/notificationMessageTemplates" + + restore_matrix: + deviceConfiguration: + backup: full + restore: enabled + risk: medium + notes: "Standard-Case für Backup+Restore; starke Preview/Audit Pflicht." + + groupPolicyConfiguration: + backup: full + restore: enabled + risk: medium + + settingsCatalogPolicy: + backup: full + restore: enabled + risk: medium + notes: "Settings are applied via configurationPolicies/{id}/settings; capability fallbacks may require manual follow-up." + + windowsUpdateRing: + backup: full + restore: enabled + risk: medium-high + + windowsFeatureUpdateProfile: + backup: full + restore: enabled + risk: high + + windowsQualityUpdateProfile: + backup: full + restore: enabled + risk: high + + windowsDriverUpdateProfile: + backup: full + restore: enabled + risk: high + + deviceCompliancePolicy: + backup: full + restore: enabled + risk: medium + notes: "Compliance-Änderungen können Zugriff beeinflussen, aber sind gut verständlich." + + appProtectionPolicy: + backup: full + restore: enabled + risk: medium-high + notes: "MAM-Änderungen wirken auf Datenzugriff in Apps; Preview und Diff wichtig." + + mamAppConfiguration: + backup: full + restore: enabled + risk: medium-high + + managedDeviceAppConfiguration: + backup: full + restore: enabled + risk: medium-high + + conditionalAccessPolicy: + backup: full + restore: preview-only + risk: high + notes: "Hohe Ausfallgefahr. v1: Backup, Versioning, Diff + ausführliche Preview; Restore nur manuell anhand Preview." + + deviceManagementScript: + backup: full + restore: enabled + risk: medium + notes: "Script-Inhalt und Einstellungen werden gesichert; Decode/Encode beachten." + + deviceShellScript: + backup: full + restore: enabled + risk: medium + + deviceHealthScript: + backup: full + restore: enabled + risk: medium + + enrollmentRestriction: + backup: full + restore: preview-only + risk: high + notes: "Kann Enrollment blockieren; v1 eher nur Preview + manuelle Umsetzung." + + windowsAutopilotDeploymentProfile: + backup: full + restore: enabled + risk: medium-high + notes: "Provisioning-kritisch; Preview + Audit, aber automatisierbar." + + windowsEnrollmentStatusPage: + backup: full + restore: enabled + risk: medium + notes: "ESP beeinflusst OOBE UX; Änderungen klar sichtbar." + + endpointSecurityIntent: + backup: full + restore: enabled + risk: high + notes: "Security-relevante Einstellungen (z. B. Credential Guard); Preview + klare Konflikt-Warnungen nötig." + + endpointSecurityPolicy: + backup: full + restore: enabled + risk: high + notes: "Enabled with template validation (Feature 023)." + + securityBaselinePolicy: + backup: full + restore: preview-only + risk: high + notes: "High risk; preview-only by default." + + mobileApp: + backup: metadata-only + restore: enabled + risk: low-medium + notes: "Nur Metadaten/Zuweisungen; kein Binary; Restore setzt Konfigurationen/Zuweisungen wieder." + + assignmentFilter: + backup: full + restore: enabled + risk: low + + roleScopeTag: + backup: full + restore: enabled + risk: low + + notificationMessageTemplate: + backup: full + restore: enabled + risk: low +``` + +## User Scenarios & Testing *(mandatory)* + +### User Story 1 - Policy inventory listing (Priority: P1) + +Admin can view supported Intune object types (as defined in the scope) with normalized metadata for selection. + +**Why this priority**: Inventory is the entry point for backup/version flows. Without it, no downstream workflows are usable. + +**Independent Test**: From Filament, navigate to Policies; verify supported types render with identifiers, type/category, platform metadata, and tenant scoping. + +**Acceptance Scenarios**: + +1. **Given** an authenticated admin, **When** they open the Policies list, **Then** they see supported object types with identifiers, type/category, platform, and last-updated metadata. +2. **Given** filtering by type/category, **When** the admin selects a type, **Then** only matching objects appear and the view remains tenant-scoped. +3. **Given** Settings Catalog Policies exist in Intune, **When** the admin opens the Policies list and syncs, **Then** Settings Catalog Policies are listed as type `Settings Catalog Policy` (settingsCatalogPolicy) and are not mixed into `Device Configuration`. +--- +### User Story 1b - Policy detail shows readable settings (Priority: P1) + +Admin can open a policy detail page and see the **effective Intune settings** in a readable, normalized way (not raw JSON dumps). + +**Independent Test**: From Filament, open a policy detail view; verify a "Settings" section renders normalized key/value pairs (or tables for special cases) derived from the latest snapshot. + +**Acceptance Scenarios**: + +1. **Given** a policy with at least one captured snapshot, **When** the admin opens the policy detail view, **Then** they see a "Settings" section rendering the policy configuration in a readable format (grouped/labeled). +2. **Given** the snapshot contains nested structures or list-based settings (e.g., OMA-URI / Settings Catalog), **When** the admin views settings, **Then** values are flattened/grouped or rendered as tables, and irrelevant metadata keys are hidden. +--- +### User Story 2 - Backup creation and browsing (Priority: P1) + +Admin creates backup sets containing multiple objects (config, compliance, scripts, apps, CA, etc.) with immutable snapshots and can browse backup details in Filament. + +**Why this priority**: Backups provide safety and enable restore; immutability and audit are foundational. + +**Independent Test**: Initiate a backup set selecting multiple objects; confirm immutable JSONB snapshots persisted, audit log written, and Filament shows backup detail and items. + + +**Acceptance Scenarios**: + +1. **Given** selected objects from different categories, **When** the admin creates a backup set, **Then** backup items store immutable payload snapshots (full or metadata-only as per the restore matrix) with identifiers and types. +2. **Given** a completed backup set, **When** the admin opens its detail page, **Then** all items and metadata display along with the audit record of creation. + +3. **Given** mehrere Backup-Sets existieren, + **When** der Admin ein Backup-Set auswählen oder ansehen möchte, + **Then** sieht er für jedes Set: + - einen sprechenden Namen (nicht nur Timestamp), + - das Erstellungsdatum, + - die Anzahl der enthaltenen Items, + - und optional eine kurze Beschreibung, damit er das Set sinnvoll unterscheiden kann. + +--- + +### User Story 3 - Version history and diff (Priority: P1) + +Admin can capture versions for any supported object, view timelines, and compare any two versions with meaningful diffs. + +**Why this priority**: Version visibility and diffs enable rollback readiness and change comprehension. + +**Independent Test**: Create multiple versions for a given object; verify timeline ordering, version metadata, and diff output (human summary + JSON diff where feasible) between any two versions. + +**Acceptance Scenarios**: + +1. **Given** an admin triggers version capture for an object, **When** the version is saved, **Then** an immutable snapshot and metadata (actor, time, type, tenant) are recorded. +2. **Given** two versions of the same object, **When** the admin requests a comparison, **Then** the UI shows a human-readable summary and structured JSON diff where available. +3. **Given** a saved policy version, **When** the admin opens the version detail page, **Then** the snapshot is displayed as pretty-printed JSON and, where possible, as normalized settings (not as an unreadable serialized array/string). +--- + +### User Story 4 - Restore with preview and confirmation (Priority: P1) + +Admin can run a restore from a backup set with preview/dry-run, selective restore, clear warnings, and required confirmation before execution. + +**Why this priority**: Restore is high-risk; safety features are mandatory for production readiness. + +**Independent Test**: Start a restore from a backup set in preview; view change summary and warnings; select items; confirm execution; verify audit logs and outcomes recorded (success/failure/partial). + +**Acceptance Scenarios**: + +1. **Given** a backup set, **When** the admin initiates a restore in preview mode, **Then** the system shows a change summary with selectable items and conflict warnings. +2. **Given** selected items and explicit confirmation, **When** execution proceeds, **Then** applied changes are tenant-scoped and audit logs record start, result, and any failures. + +3. **Given** mehrere Backup-Sets existieren, + **When** der Admin einen Restore Run erstellt, + **Then** zeigt die Auswahl für das "Backup set" mindestens: + - den Backup-Namen, + - das Erstellungsdatum, + - die Anzahl der Items, + damit der Admin das richtige Backup-Set sicher auswählen kann. + +4. **Given** ein Restore Run wurde erstellt, + **When** der Admin die Detailseite des Restore Runs öffnet, + **Then** sieht er, welche Policies/Items in diesem Run enthalten sind + (z. B. Liste der Policies mit Name/Typ/Plattform). +--- + +### User Story 5 - Operational readiness and environments (Priority: P2) + +Local development uses Sail; deployments target Dokploy staging then production with clear validation steps. + +**Why this priority**: Ensures reproducible local setup and safe promotion to production. + +**Independent Test**: Run the app locally via Sail; validate migrations on staging before production; confirm required env vars and queues/workers are documented. + + + + +### User Story 6 - Berechtigungsübersicht & Health-Status (Priority: P1) + +Als Admin möchte ich für jeden Tenant sehen, welche Microsoft Graph-Berechtigungen +erforderlich sind, welche bereits erteilt wurden und welche fehlen, damit ich +sicherstellen kann, dass alle Funktionen von TenantPilot sicher und vollständig +arbeiten. + +**Why this priority**: Jede neue Funktion kann zusätzliche Berechtigungen benötigen. +Ohne transparente Übersicht und Abgleich besteht das Risiko, dass Features still +kaputt sind oder unsicher laufen. + +**Acceptance Scenarios**: + +1. **Given** ein Tenant ist in TenantPilot hinterlegt, + **When** der Admin die Tenant-Detailseite öffnet, + **Then** sieht er eine Liste aller *erforderlichen* Berechtigungen mit Status + (z. B. OK, fehlt). + +2. **Given** neue Funktionen wurden eingeführt, die zusätzliche Berechtigungen benötigen + und diese wurden in der zentralen Permissions-Liste hinzugefügt, + **When** der Admin die Tenant-Detailseite öffnet, + **Then** erscheinen die neuen Berechtigungen automatisch in der Übersicht und + fehlende Berechtigungen werden klar als fehlend markiert. + +3. **Given** der Admin klickt auf "Verify configuration", + **When** TenantPilot einen Graph-Twestcall und/oder das Permission-Setup prüft, + **Then** wird der Status der Berechtigungen aktualisiert (OK/fehlt/Fehler) und + es wird ein Audit-Eintrag erstellt. + +4. **Given** ein Tenant hat fehlende kritische Berechtigungen, + **When** andere Features (Policy-Sync, Backup, Restore) diesen Tenant verwenden, + **Then** kann TenantPilot dem Admin entsprechende Warnungen anzeigen oder die + Funktion mit einem klaren Fehler abbrechen. + + +**Acceptance Scenarios**: + +1. **Given** a fresh checkout, **When** Sail commands run (`./vendor/bin/sail up -d`, `./vendor/bin/sail artisan migrate`), **Then** the app boots with PostgreSQL and Filament admin available. +2. **Given** a pending release, **When** migrations and restore flows are validated on staging, **Then** production deployment proceeds with documented steps and environment parity. + + ### User Story 8 – Graph Contract Registry & Drift Guard (Priority: P1) +Admin soll sich darauf verlassen können, dass Backup/Restore/Preview nicht wegen Graph-Shape-Details (derived @odata.type, verbotene $expand/$select, Property-Abweichungen) “random” bricht. + +Acceptance Scenarios: + 1. Given ein Backup enthält @odata.type = #microsoft.graph.windows10CompliancePolicy, +When Preview/Restore läuft, +Then wird das als gültiger deviceCompliancePolicy-Family Typ behandelt (kein odata_mismatch), und der korrekte Endpoint/Method wird genutzt. + 2. Given ein Endpoint erlaubt bestimmte Expands/Selects nicht, +When TenantPilot Requests baut, +Then werden nur “allowed capabilities” verwendet (kein 400 durch OData parsing). + 3. Given Microsoft/Intune ändert Shape/Capabilities, +When graph:contract:check läuft, +Then schlägt der Check kontrolliert fehl und zeigt welcher Contract angepasst werden muss (statt dass Prod-Flows brechen). + +2) Neue Functional Requirements (FR-03x) ergänzen + +Beispiel, passend zu deinem Stil: + • FR-031: System MUST maintain a central Graph Contract Registry per supported type/endpoint (resource path, allowed $select, allowed $expand, “type family” / allowed @odata.type values, create/update methods). + • FR-032: Restore/Preview MUST treat derived @odata.type values as compatible within a declared type family (e.g. compliance policy family), and MUST NOT hard-fail on base-vs-derived mismatches. + • FR-033: System MUST provide a verification command (e.g. php artisan graph:contract:check) that validates registry assumptions against live Graph behavior (at least via canary calls / lightweight probes), logging actionable diffs. + • FR-034: When Graph returns capability errors (OData select/expand, unsupported features), system MUST downgrade to a safe fallback strategy (e.g. “no expand, extra fetches”) and MUST record a warning/audit entry. + +(Du kannst FR-033 “live check” auch optional machen für prod, aber mindestens in CI/Staging wertvoll.) + +3) Implementation Notes / Data Artefacts ergänzen + +Ein kleines, versioniertes Artefakt einführen, z. B.: + • config/graph_contracts.php oder .specify/contracts/graph.yaml + +Darin pro Objekt-Typ: + • resource (collection + single-item path) + • type_family (Liste erlaubter @odata.type) + • allowed_select / allowed_expand + • member_hydration_strategy (z. B. “property array” vs “subresource” vs “not available”) + • create_method / update_method / id_field + +Das verhindert “Wissens-Leaks” quer durch Services. +### Edge Cases + +- Graph permissions missing or expired, causing policy fetch/restore failures with clear error mapping and audit entries. +- Large policy payloads or many items in a backup set; ensure JSONB storage and pagination handle load without timeouts. +- Restore conflicts when target tenant already has newer versions; preview must surface warnings and allow skip. +- Partial restore failures; audits must capture per-item outcomes and surface retry guidance. +- Diff generation for incompatible or malformed payloads should fail gracefully with admin-facing messaging. +- Retention/size concerns for snapshots; document defaults and guard against unbounded growth. +- Snapshots stored as serialized strings or array-only dumps (keys lost) must be detected; UI should show a clear warning and fall back to raw display. +- Policies whose `@odata.type` does not match the expected platform/type mapping should be flagged to prevent wrong restore previews (e.g., stored as Windows but snapshot indicates Android). +## Requirements *(mandatory)* + +### Functional Requirements + +- **FR-001**: System MUST list all Intune objects defined in the `scope.supported_types` section with normalized metadata and tenant scoping for selection. +- **FR-002**: System MUST allow admins to create backup sets containing multiple objects (configuration, compliance, scripts, apps, conditional access, etc.) with immutable JSONB payload snapshots. +- **FR-003**: Backup creation MUST log audit events including actor, timestamp, tenant, items, and outcome. +- **FR-004**: System MUST capture policy versions on demand and present per-policy timelines. +- **FR-005**: Users MUST be able to diff any two versions with a human-readable summary and structured JSON diff where feasible. +- **FR-006**: Restore MUST support preview/dry-run, selective item restore, and explicit confirmation before applying changes, within the per-type restore level defined in `scope.restore_matrix`. +- **FR-007**: Restore execution MUST produce audit logs covering success, failure, and partial outcomes. +- **FR-008**: Graph integration MUST route through a dedicated abstraction layer with standardized error mapping, safe retries, and high-level logging without secrets. +- **FR-009**: All policy, version, backup, and restore data MUST be tenant-aware; queries enforce tenant isolation. +- **FR-010**: Application MUST run locally via Laravel Sail with PostgreSQL and provide Filament admin flows. +- **FR-011**: Deployments MUST target Dokploy staging before production with documented migration and worker implications. +- **FR-012**: Tests MUST cover backup composition rules, version immutability, audit events, and Filament backup/restore flows (with Graph boundaries mocked). +- **FR-013**: Raw policy snapshots and backup payloads MUST be stored as JSONB with indexes justified by query needs (e.g., FK and time-based; GIN when filters require). +- **FR-014**: UI MUST provide clear warnings for potential restore conflicts and require confirmation for destructive operations; for types with `restore: preview-only` in `scope.restore_matrix` no direct apply action MAY be offered. +- **FR-015**: Admins MUST be able to safely delete (archive) backup sets that are no + longer needed. Deletion is implemented as soft-delete with audit logging, and + backup sets referenced by completed restore runs cannot be removed. + +- **FR-016**: Admins MUST be able to delete individual policy versions for housekeeping. + Deletion is implemented as soft-delete with audit logging. + +- **FR-017**: Admins MUST be able to deactivate (soft-delete) a tenant. +Deactivated + tenants: + - do not appear in default lists, + - cannot be used for new sync/backup/restore operations, + - keep their historical data and audit logs for traceability. +- **FR-018**: Admins MAY soft-delete restore runs to keep the UI clean; underlying + backup and policy data remains untouched. + +- **FR-019**: The system MUST normalize different payload structures for display via a `PolicyNormalizer` (or equivalent): OMA-URI/custom policies as path/value tables, Settings Catalog policies as flattened structures, and standard objects as key-value views, aligned with `scope.supported_types`. +- **FR-019a**: Policy detail views MUST display a "Settings" section derived from the latest available snapshot (using the normalizer output when available). +- **FR-019b**: Policy version detail views MUST render snapshots as pretty-printed JSON (monospace, copyable) and SHOULD also render normalized settings via the same normalizer. +- **FR-020**: For PowerShell script objects (`deviceManagementScript` in `scope.supported_types`), the `scriptContent` MUST be base64-decoded when stored in backups/versions for readability/diffing and encoded again when sent back to Graph during restore. +- **FR-021**: Restore behavior MUST follow the per-type configuration in `scope.restore_matrix`: `backup` determines full vs metadata-only snapshots; `restore` determines whether automated restore is enabled or preview-only; `risk` informs warning/confirmation UX. +- **FR-022**: For high-risk types with `restore: preview-only` in `scope.restore_matrix` (e.g., `conditionalAccessPolicy`, `enrollmentRestriction`), TenantPilot MUST provide full backups, version history, and diffs plus detailed restore previews, but MUST NOT expose direct Graph apply actions; restore is manual, guided by the preview. +- **FR-036**: When `settingsCatalogPolicy` settings apply fails because the Graph settings endpoint is unsupported (route missing / method not allowed), the system MUST attempt a safe fallback by creating a new policy from the snapshot and record the new policy id. If creating with settings is not supported, the system MUST retry with a metadata-only payload, mark the restore item as partial, and surface a manual settings-apply warning. + +### Key Entities *(include if feature involves data)* + +- **tenants**: Represents the deployment tenant context; referenced by all scoped data. +- **policies**: Normalized metadata for supported Intune policies. +- **policy_versions**: Immutable snapshots with metadata (actor, timestamp, tenant, policy type). +- **backup_sets**: Group of backup items with creator, timestamp, and tenant context. +- **backup_items**: Individual policy snapshots within a backup set (immutable JSONB payload + identifiers). +- **restore_runs**: Execution records for restores, including preview/actual flags and outcomes. +- **audit_logs**: Audit trail entries for backups, restores, version captures, and significant Graph actions. + +## Success Criteria *(mandatory)* + +### Measurable Outcomes + +- **SC-001**: Admin can create a backup set selecting multiple policies and view immutable backup items with audit logs in Filament. +- **SC-002**: Policy version history timeline is available per policy and supports comparing any two versions with summary and JSON diff outputs. +- **SC-003**: Restore preview shows change summaries and conflict warnings; execution requires explicit confirmation and produces audit logs for all outcomes. +- **SC-004**: Core flows run locally via Sail; staging validation of migrations and restore paths completes before production deployments. +- **SC-005**: Automated tests covering backup composition, version immutability, audit logging, and Filament backup/restore flows pass via `./vendor/bin/sail artisan test`. + + +### Technical Story – Enforce Single Current Tenant ("Highlander Principle") + +**Context** + +Aktuell können mehrere Tenants `status = active` sein. Graph-Operationen (Policy Sync, +Backup, Restore) wählen den Kontext über Heuristiken (`findOrCreateDefault`, +`local-tenant`), was zu falschen Tenants und Fehlern führt. + +**Goal** + +Es soll **immer genau einen klar definierten "current" Tenant** geben, über den +alle Graph-Operationen laufen. Die Auswahl dieses Tenants ist explizit und +transparent (UI + Env), nicht implizit. + +**Requirements** + +- Es gibt ein Flag `is_current` in `tenants`, das den aktuell verwendeten Kontext + markiert. +- Die Datenbank erzwingt per partiellem Unique Index, dass höchstens ein + nicht-gelöschter Tenant `is_current = true` haben kann. +- `Tenant::current()` liefert: + - falls `INTUNE_TENANT_ID` gesetzt ist, **genau diesen** Tenant (Fehler, wenn + er nicht existiert oder deaktiviert ist), + - sonst den Tenant mit `is_current = true` und `status = active`. + - falls keiner gefunden wird, eine klare Exception (“No current tenant selected”); + es werden keine Dummy-Tenants erzeugt. +- In der Tenant-Verwaltung gibt es eine Action "Make current", die: + - in einer Transaktion alle anderen Tenants auf `is_current = false` setzt + und den gewählten Tenant auf `is_current = true`, + - nur für aktive Tenants verfügbar ist. +- Der frühere Placeholder `local-tenant` darf nicht mehr als Graph-Kontext genutzt + werden; sobald ein echter Tenant existiert, wird er archiviert und ist nie + `is_current`. +- Alle Graph-basierten Funktionen (Policy Sync, Backup, Restore) verwenden + konsistent `Tenant::current()` oder einen explizit übergebenen Tenant. + + Tenant-level actions such as "Admin consent" and "Verify configuration" +MUST be exposed on the tenant detail view (and/or row actions), not as a +global button without explicit tenant context. + + +### UX Guideline – Table Actions / Dropdowns + +- Tabellen in Filament mit mehr als zwei Zeilen-Aktionen (z.B. View, Edit, + Admin consent, Verify, Deactivate, Force delete) MÜSSEN ihre Aktionen in + einem kompakten Dropdown / ActionGroup bündeln, statt alle Buttons nebeneinander + anzuzeigen. +- Ausnahmen: besonders häufige, nicht-destruktive Aktionen (z.B. "View") + dürfen weiterhin als einzelner Button sichtbar bleiben; alle weiteren + Aktionen (z.B. Admin-Aktionen, Housekeeping) sollen im Dropdown liegen. +- Ziel: die Tabellen bleiben übersichtlich, Spaltenbreite wird begrenzt, + und Admins bekommen eine konsistente "⋯"-Interaktion für erweiterte Aktionen. + + + +## User Story 7 – Intune RBAC Onboarding Wizard (Delegated Admin Login) *(Priority: P1)* + +### Problem / Context + +TenantPilot arbeitet primär **app-only (Client Credentials)** gegen Microsoft Graph. +Für viele Intune-Objekte reicht „Graph App Permissions + Admin Consent“ allein nicht aus: Intune kann zusätzlich über **Intune RBAC** +blockieren, wenn der **Service Principal** (Enterprise App) keine passende **Intune Role Assignment** inkl. Scope hat. +Das äußert sich typischerweise als **403** mit „Application is not authorized to perform this operation“. + +Dieses Setup ist ein **Bootstrap-Problem**: +- Ohne RBAC-Zuweisung sind Intune Reads/Writes blockiert. +- Ohne ausreichende Rechte kann TenantPilot die RBAC-Zuweisung nicht „self-service“ per app-only herstellen. + +**Ziel:** TenantPilot bietet pro Tenant einen **Onboarding-Wizard**, bei dem ein Admin sich **interaktiv (delegated)** anmeldet, +und TenantPilot automatisiert (idempotent) die erforderliche Intune-RBAC-Konfiguration für die konfigurierte Enterprise App herstellt. +Danach funktionieren Policy Sync / Backup / Restore (gemäß Restore Matrix) zuverlässig. + +--- + +### User Value + +- Admins können RBAC-Probleme direkt in TenantPilot beheben (kein “Portal-Rätselraten”). +- Klarer, auditierter Ablauf (wer hat wann welche Rechte/Sopes gesetzt). +- Minimiert Ausfälle bei Policy-Sync/Backup/Restore und reduziert Support-Aufwand. + +--- + +### In Scope (v1) + +- Wizard in Filament auf der **Tenant-Detailseite** (tenant-scoped). +- Delegated Admin Login (interaktiv). +- Idempotente Ausführung: + - Service Principal (zu `tenant.app_client_id`) auflösen + - RBAC-Membership via **Security Group (recommended)** herstellen + - Intune Role Assignment erstellen/aktualisieren (Rolle + Scope) + - Abschließender Verify-Run (Health/Permissions aktualisieren) +- Vollständige Audit-Logs pro Step. + +--- + +### Out of Scope (v1) + +- Vollautomatisches “Self-heal” ohne Admin-Interaktion. +- Zeitgesteuerte Jobs, die RBAC-Rechte vergeben (ohne explizite Admin-Aktion). +- Unterstützung mehrerer paralleler RBAC-Profile pro Tenant (nur ein “recommended setup”). + +--- + +## UX / Entry Points + +### Entry Point: Tenant Detail (Filament) + +Auf der `TenantResource` Detailseite im Action-Dropdown: + +- `Setup Intune RBAC` (Wizard) +- `Admin consent` +- `Verify configuration` + +**Visibility rules:** +- Nur für `status=active` Tenants. +- Nur wenn `app_client_id` gesetzt ist. +- Optional: Badge/Hint “RBAC missing” aus Health-Check. + +**Copy/Help:** +- Kurze Erklärung: “Graph Permissions ≠ Intune RBAC”. +- Hinweis auf Least Privilege. +- Klarer Hinweis, dass Änderungen tenantweit wirken (je nach Scope). + +--- + +## Wizard Flow + +### Step 1 — Configuration (Role / Scope / Group) + +**Inputs:** +- **Role** (Dropdown): + - Default: `Policy and Profile Manager` (Least Privilege für Policy/Config-Workflows) + - Optional: `Intune Administrator` (mit Warnung) +- **Scope** (Dropdown): + - Default: `Global / All devices` (wenn verfügbar) + - Optional: Auswahl einer Scope Group / Device Group (falls euer Modell das nutzt) +- **Group Mode**: + - Default: `Use Security Group (recommended)` + - Options: + - `Create new group` (Default-Name: `TenantPilot-Intune-RBAC`) + - `Use existing group` (Picker) + +**UI Requirements:** +- “Review screen” zeigt *genau*, was erstellt/geändert wird (Role, Scope, Group). + +### Step 2 — Delegated Admin Login + +- Admin führt interaktiven Login durch (delegated). +- Wizard zeigt klar: + - welcher Tenant + - welche App (Client ID / Display Name, sofern auflösbar) + - dass nur kurzzeitig ein User-Token genutzt wird + +**Security rule (mandatory):** +- Delegated Access Tokens werden **nicht persistiert** (keine Speicherung in DB/Cache). +- Tokens existieren nur im Request-Kontext / Session und werden nach Abschluss verworfen. + +### Step 3 — Execute Setup (Idempotent + Safe) + +Wizard führt folgenden Ablauf aus (alle Operationen tenant-scoped, über Graph-Abstraktion, mit Error-Mapping): + +1) **Resolve Service Principal** +- Auflösen des Service Principals zur `tenant.app_client_id`. +- Wenn nicht gefunden: + - Wizard stoppt mit Hinweis: “Enterprise App ist im Tenant nicht vorhanden. Bitte zuerst Admin Consent durchführen.” + - Audit log: `tenant.rbac.setup.failed` (reason: sp_not_found) + +2) **Ensure Security Group** +- Falls “Create new group”: + - Security Group erstellen (securityEnabled=true, mailEnabled=false). + - Wenn bereits vorhanden (gleiches displayName): wiederverwenden (oder per gespeicherter `rbac_group_id`). +- Falls “Use existing group”: + - Validieren: `securityEnabled=true`. +- Ergebnis-IDs werden gespeichert: + - `tenants.rbac_group_id` (neu, optional) + - `tenants.rbac_group_name` (optional, nur für UX) + +3) **Ensure Membership (SP ∈ Group)** +- Service Principal als Member hinzufügen, wenn nicht vorhanden. +- Konflikte (already exists) müssen als OK behandelt werden. + +4) **Ensure Intune Role Assignment** +- Suche nach existierendem Role Assignment, das: + - die gewünschte RoleDefinition referenziert + - die Group als Member enthält + - den gewünschten Scope abdeckt +- Wenn vorhanden: **Patch/Update** (z. B. Scope ergänzen) +- Wenn nicht vorhanden: **Create** Role Assignment + +5) **Post-Verify (mandatory)** +- Direkt nach Setup: + - `Verify configuration` ausführen (inkl. Permission-Matrix Update) + - Zusätzlich 1–2 “Canary Calls” gegen Intune-Endpunkte, die für v1 kritisch sind (Read-Only reicht). +- Ergebnisse werden in Tenant-Health gespeichert (`app_status`, permissions health). + +**Execution mode (v1):** +- Wizard führt die Steps synchron aus (kein Queue-Job), um Token-Probleme zu vermeiden. +- Timeouts: klare Fehlermeldung + Audit. + +### Step 4 — Summary + +- Wizard zeigt: + - Group (Name + ObjectId) + - Role (Name) + - Scope (global / group id) + - RoleAssignmentId (falls verfügbar) + - Verify result (OK / Partial / Failed) +- CTA: “Retry policy sync” + +--- + +## Data Model Additions (minimal) + +Erweiterung `tenants` (optional aber empfohlen, für Transparenz & Idempotenz): + +- `rbac_group_id` (nullable string/GUID) +- `rbac_role_assignment_id` (nullable string/GUID) +- `rbac_role_key` (nullable string; z. B. `policy_profile_manager`) +- `rbac_scope_mode` (nullable string; z. B. `global|group`) +- `rbac_scope_id` (nullable string/GUID) + +> Hinweis: Wenn ihr strikt ohne zusätzliche Felder arbeiten wollt, geht es auch rein über Discovery, +> aber gespeicherte IDs machen den Wizard deutlich stabiler und schneller. + +--- + +## Functional Requirements (additions) + +- **FR-023**: System MUST expose a per-tenant onboarding wizard “Setup Intune RBAC” in Filament. +- **FR-024**: Wizard MUST use delegated admin login and MUST NOT store delegated tokens. +- **FR-025**: Wizard MUST be idempotent (re-run safe) and MUST converge to the desired RBAC state. +- **FR-026**: Wizard MUST support group-based RBAC membership (recommended) and MUST ensure the service principal is a member. +- **FR-027**: Wizard MUST create or update Intune role assignments with an explicit role + scope. +- **FR-028**: Wizard MUST run a post-setup verification that updates tenant health and permissions UI. +- **FR-029**: Wizard MUST write audit logs for start, each step outcome, and final result (success/failed/partial). +- **FR-030**: Wizard MUST enforce tenant isolation and use explicit tenant context (no implicit defaults). + +--- + +## Non-Functional / Safety Requirements + +- Least Privilege: + - Default role selection is non-global admin (Policy/Profile manager). + - Selecting higher-privilege roles shows a warning and requires explicit confirmation. +- Clear Failure UX: + - Every failure must map to an actionable message (e.g., “Admin consent missing”, “Insufficient directory permissions”). +- No secrets: + - No access tokens, secrets, or payloads in logs/audits. +- Deterministic logging: + - Audit entries include tenant_id, actor_user_id, action_key, resource IDs (group, roleAssignment), and status. + +--- + +## Acceptance Scenarios + +1) **Missing RBAC → Wizard fixes it** +- **Given** ein aktiver Tenant mit konfigurierter App (`app_client_id`) und Admin Consent, + aber Intune Calls liefern RBAC-403, +- **When** der Admin den Wizard ausführt, +- **Then** wird Gruppe+Membership+RoleAssignment hergestellt, Verify wird OK, und Policy Sync funktioniert. + +2) **Admin Consent fehlt** +- **Given** `app_client_id` ist gesetzt, aber der Service Principal kann nicht aufgelöst werden, +- **When** Wizard startet, +- **Then** bricht er mit “Bitte zuerst Admin Consent durchführen” ab und schreibt Audit `sp_not_found`. + +3) **Idempotenz** +- **Given** Wizard wurde bereits erfolgreich ausgeführt, +- **When** Wizard erneut mit gleichen Einstellungen ausgeführt wird, +- **Then** werden keine Duplikate erzeugt, und die Summary zeigt “No changes / Already compliant”. + +4) **Insufficient privileges** +- **Given** ein Admin loggt sich ein, aber hat nicht die nötigen Rechte, +- **When** Setup ausgeführt wird, +- **Then** stoppt der Wizard mit klarer Fehlermeldung pro Step (z. B. group create / role assignment create), + und Audit enthält den Step und Fehlercode. + +5) **Restricted scope** +- **Given** Admin wählt eine eingeschränkte Scope Group, +- **When** Setup abgeschlossen ist, +- **Then** Verify markiert ggf. “Partial” mit Hinweis “Inventory limited by scope”. + +--- + +## Implementation Notes (for plan.md linkage) + +- Reuse existing services pattern: + - `IntuneRbacSetupService` (new) in `app/Services/Intune/` + - Uses `GraphClientInterface` and existing error mapping/logging hooks. + - Uses `AuditLogger` for stepwise audit events. +- Extend `TenantPermissionService` (User Story 7 in tasks) to include an RBAC check state: + - status: `ok|missing|error` + - message: “Intune RBAC role assignment missing (Wizard required)” +- Add Filament wizard page/action under `TenantResource`. + +--- + +## Edge Cases + +- Group exists but is not security-enabled → fail with actionable message. +- Role assignment exists but wrong scope → patch and warn. +- Multiple “similar” groups by name → prefer stored `rbac_group_id` if present, else prompt. +- Tenant mismatch: Wizard must never operate on non-selected tenant (enforce `Tenant::current()` or explicit tenant param). +- Token expiry mid-run → show “Please retry” + audit partial. + + +Previous draft archived under spechistory/spec.md diff --git a/specs/000-initial-config/tasks.md b/specs/000-initial-config/tasks.md new file mode 100644 index 0000000..31b93d1 --- /dev/null +++ b/specs/000-initial-config/tasks.md @@ -0,0 +1,912 @@ +--- +description: "Task list for TenantPilot v1 implementation" +--- + +# Tasks: TenantPilot v1 (ARCHIVED) + +> ARCHIVED / legacy copy. +> Do not use this folder for active work. +> Active feature task lists live under `specs/-/` on `feat/-` branches. + +**Input**: Design documents from `.specify/spec.md` and `.specify/plan.md` +**Prerequisites**: plan.md (complete), spec.md (complete) + +**Status snapshot** +- Done: Phases 1–15 (US1–US8, Settings Catalog hydration/display, restore rerun, Highlander, US6 permissions/health, housekeeping/UX, ops) +- Open: T167 (optional) CLI/Job for CHECK/REPORT only (no grant) +- Next up: Feature 018 (Driver Updates / WUfB add-on) in `specs/018-driver-updates-wufb/` + +--- + +## 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. +- [x] 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`. +- [x] 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. + +- [x] 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: + - `displayName` → `name` + - (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). + +- [x] T179 [US1b][Scope][settingsCatalogPolicy] Hydrate Settings Catalog “Configuration settings” for snapshots + normalized display + +- **Goal:** Für `settingsCatalogPolicy` sollen die **Configuration settings** (wie im Intune Portal unter *Configuration settings*) im System sichtbar sein: + - in **Policy Version Raw JSON** enthalten + - im **Normalized settings** Abschnitt als verständliche Liste/Tabelle dargestellt + - damit Diff/Preview/Restore auf den relevanten Settings basiert (nicht nur “General” Metadaten). + +- **Why:** `deviceManagement/configurationPolicies` liefert im Base-Entity oft nur Metadaten (`name`, `platforms`, `technologies`, `settingCount`, `templateReference` …). Die eigentlichen Settings liegen typischerweise in einem **Subresource** (z. B. `.../configurationPolicies/{id}/settings`). Aktuell zeigt TenantPilot daher nicht die relevanten Werte (PIN length, biometrics, etc.). + +--- + +## Implementation + +### 1) Graph Contract Registry erweitern (Hydration Strategy) +- In `config/graph_contracts.php` beim Contract für `settingsCatalogPolicy` ergänzen: + - `member_hydration_strategy: 'subresource_settings'` + - `subresources`: + - `settings`: + - `path`: `deviceManagement/configurationPolicies/{id}/settings` + - `collection`: true + - `paging`: true + - `allowed_select`: konservativ (oder leer → fallback) + - `allowed_expand`: leer + +> Erwartung: Registry definiert, wie der Snapshot “vollständig” gemacht wird. + +### 2) Snapshot Capture für settingsCatalogPolicy hydrieren +- In dem Service, der Snapshots für **Version/Backup/Restore-Preview** lädt (z. B. `BackupService`, `VersionService`, `RestoreService` oder zentraler “PolicySnapshotService” falls vorhanden): + - Wenn `type_key === settingsCatalogPolicy`: + 1. `GET deviceManagement/configurationPolicies/{id}` (Base entity) + 2. `GET deviceManagement/configurationPolicies/{id}/settings` (paged) + 3. Im Snapshot speichern als **entweder**: + - `snapshot['settingsCatalog'] = ['settings' => [...]]` + - **oder** `snapshot['settings'] = [...]` (wenn konsistenter mit Normalizer) + - Wichtig: **Keine Secrets** loggen, Payload bleibt JSONB. + - Pagination: `$top` + `@odata.nextLink` sauber abarbeiten. + +### 3) PolicyNormalizer erweitern (Settings Catalog wirklich anzeigen) +- In `app/Services/Intune/PolicyNormalizer.php`: + - Für `settingsCatalogPolicy` nicht nur Metadaten anzeigen, sondern: + - `settings` / `settingsCatalog.settings` interpretieren + - pro Setting mindestens: + - Setting-Name/Display (wenn vorhanden) + - Setting-Path/DefinitionId (wenn vorhanden) + - Value (aktueller Wert) + - Ausgabe als Tabelle/RepeatableEntry (“Key/Value”), gruppiert nach Kategorie (z. B. “Windows Hello for Business”), wenn ableitbar. + +### 4) “Settings available” Badge korrekt setzen +- Stelle sicher, dass die Logik “Settings available” bei `settingsCatalogPolicy` erst dann **Available** zeigt, wenn der Snapshot **Settings** enthält (nicht nur Base entity). +- Optional: Status “Partial”, wenn Base ok ist, aber Settings fetch fehlgeschlagen. + +### 5) Diff & Restore Preview profitieren lassen (Ziel, kein Muss) +- Diff/Preview soll aus dem hydrierten Snapshot arbeiten → Änderungen an Settings werden sichtbar. +- Falls Diff aktuell nur top-level vergleicht: sicherstellen, dass `settings` Teil des Snapshot-Diffs ist. + +### 6) Permissions/Health prüfen +- Prüfen, ob der `.../settings` Endpoint zusätzliche Permissions braucht. + - Falls ja: `config/intune_permissions.php` ergänzen + Verify/Health zeigt Missing sauber an. + +--- + +## Tests (Pest) + +### Feature: `tests/Feature/Filament/SettingsCatalogPolicyHydrationTest.php` +- Seed Policy vom Typ `settingsCatalogPolicy` +- Mock Graph: + - Base entity call liefert Metadaten + - `/settings` liefert 2–3 Settings Objekte (mit Value) +- Trigger Snapshot Capture (Version oder Backup) +- Assert: + - Snapshot enthält `settings` / `settingsCatalog.settings` + - Policy Version Detail zeigt Normalized settings mit diesen Einträgen + +### Unit: `tests/Unit/PolicyNormalizerSettingsCatalogTest.php` +- Input: Snapshot mit `settings` Array +- Assert: Normalizer liefert strukturierte Key/Value Ausgabe, nicht nur Metadaten + +### Regression +- `deviceConfiguration` / `deviceCompliancePolicy` etc. bleiben unverändert. + +--- + +## Verification + +- `./vendor/bin/pest tests/Feature/Filament/SettingsCatalogPolicyHydrationTest.php` +- `./vendor/bin/pest tests/Unit/PolicyNormalizerSettingsCatalogTest.php` +- `./vendor/bin/pint --dirty` + + + +- [x] T180 [US1b][Bug][settingsCatalogPolicy] Hydrate Settings Catalog settings in Version capture + Policy detail uses hydrated snapshot + +- **Goal:** `settingsCatalogPolicy` soll die *Configuration settings* nicht nur in Backups, sondern auch in **Policy Versions** enthalten, damit **Policy Detail**, Diff/Preview/Restore auf den echten Settings basieren. +- **Why:** Aktuell hydriert nur `BackupService`, aber Policy Detail/Versions zeigen weiterhin nur Base-Metadaten. + +### Implementation +1) **VersionService (oder zentraler SnapshotFetcher) erweitern** +- Beim Version-Capture für `settingsCatalogPolicy`: + - `GET deviceManagement/configurationPolicies/{id}` + - `GET deviceManagement/configurationPolicies/{id}/settings` (paging + nextLink) + - Merge in Snapshot unter **dem Key den Normalizer nutzt**: + - bevorzugt: `snapshot['settings'] = [...];` +- Keine Secrets loggen; nur IDs/Status im Audit. + +2) **Policy Detail nutzt latest Version Snapshot** +- Sicherstellen, dass `PolicyResource -> ViewPolicy` / Normalizer den **latest policy_version snapshot** nimmt (nicht nur Policy-Metadaten). +- Falls bereits so: nur sicherstellen, dass der Snapshot-Key konsistent ist (`settings`). + +3) **PolicyNormalizer: settingsCatalogPolicy Rendering** +- Falls bereits vorhanden: Normalizer liest `snapshot['settings']`. +- Falls nicht: ergänzen, damit in der UI eine Tabelle/Liste entsteht: + - Setting name / definitionId / value (mindestens) + +4) **Settings Badge Logik** +- Badge “Settings available” soll bei settingsCatalogPolicy nur **Available** sein, wenn `snapshot['settings']` **nicht leer** ist. +- Optional: “Partial”, wenn Base ok aber settings fetch fehlgeschlagen. + +### Tests (Pest) +- **Feature:** `tests/Feature/Filament/SettingsCatalogPolicyVersionHydrationTest.php` + - Mock Graph base entity + `/settings` + - Trigger **Version capture** + - Assert: Version Raw JSON enthält `settings` + - Assert: Policy Detail “Normalized settings” zeigt konkrete Settings (z. B. PIN length / biometrics) +- **Unit:** erweitere `PolicyNormalizerSettingsCatalogTest.php` falls nötig (Key + rendering) + +### Verification +- `./vendor/bin/pest tests/Feature/Filament/SettingsCatalogPolicyVersionHydrationTest.php` +- `./vendor/bin/pest tests/Unit/PolicyNormalizerSettingsCatalogTest.php` +- `./vendor/bin/pint --dirty` + +### Acceptance Criteria +- Policy Version Raw JSON enthält Settings (nicht nur Metadaten). +- Policy Detail zeigt konkrete Settings (z. B. “Minimum PIN Length: 12”, “Allow biometrics: True”). +- Settings Badge ist **Available**, sobald hydrierte Settings im Snapshot vorhanden sind. +- Keine Regression bei bestehenden Typen. +--- + +## Acceptance Criteria + +- Settings Catalog Policies zeigen im **Policy Version Raw JSON** die **Settings** (nicht nur Metadaten). +- Im **Normalized settings** Bereich erscheinen konkrete Werte (z. B. “Minimum PIN Length: 12”, “Allow biometrics: True”). +- “Settings” Badge ist **Available**, sobald hydrierte Settings im Snapshot vorhanden sind. +- Keine Änderungen/Regressions bei bestehenden Typen. + + + + + +- [x] 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): + - `deviceManagementConfigurationSimpleSettingInstance` → `simpleSettingValue.value` + - `deviceManagementConfigurationChoiceSettingInstance` → `choiceSettingValue.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. + + +- [x] 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 `
` 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).
+
+
+
+- [x] 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).
+
+
+
+- [x] 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.
+
+### Discovery → Decision
+
+- Checked available Graph paths and contract registry: `configurationPolicies` exposes a subresource at `deviceManagement/configurationPolicies/{id}/settings` which is the supported method to add/update settings for Settings Catalog policies. There is no special action; the supported mechanism is a POST to the settings subresource (collection) or the collection resource when creating a new policy. Therefore the restore flow will:
+  - PATCH top-level metadata (`name`, `description`) via the policy resource
+  - POST settings to `deviceManagement/configurationPolicies/{id}/settings` when present
+  - If the tenant/API rejects the settings POST (NotSupported/ModelValidationFailure), the restore item will be marked `manual_required` with Graph request IDs and a clear admin message.
+
+---
+
+## 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 2–4 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`/`_false` → `True`/`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.)
+
+- [x] T186 [US4][Bugfix][settingsCatalogPolicy] Fix settings_apply payload typing (@odata.type) + body shape for configurationPolicies/{id}/settings
+
+**Goal:** Restore für `settingsCatalogPolicy` soll Settings zuverlässig anwenden können, ohne ModelValidationFailure wegen fehlender/entfernter `@odata.type`.
+
+**Why:** Aktuell schlägt `settings_apply` fehl mit „choiceSettingValue does not exist on type …SettingInstance“ → typischerweise fehlt `@odata.type` in `settingInstance` (oder in nested children) nach Sanitizing/Mapping.
+
+### Implementation
+1. **Contract:** Ensure `settings_apply` schema is explicit in `config/graph_contracts.php` (method = `POST`, path = `deviceManagement/configurationPolicies/{id}/settings`, `body_shape` = `collection`).
+2. **Sanitizer:** In `GraphContractRegistry` allow and preserve `@odata.type` inside `settingInstance` and nested children (recursively); continue to strip read-only/meta fields and `id`.
+3. **RestoreService:** Build `settingsPayload = sanitizeSettingsApplyPayload(snapshot['settings'])` and `POST` to the contract path; on failure mark item `manual_required` and persist Graph meta (`request_id`, `client_request_id`, error message).
+4. **UI:** RestoreRun Results view shows clear admin message when `manual_required` due to settings_apply, including request ids.
+5. **Fallback create:** If the settings apply call fails with route missing / method not allowed, create a new Settings Catalog policy via `POST deviceManagement/configurationPolicies` using a sanitized payload that includes the settings. If Graph returns `NotSupported`, retry with a metadata-only payload (no settings) and mark the item as partial with a manual settings apply warning. Record the new policy id in restore results.
+
+### Tests (Pest)
+- Unit: `tests/Unit/GraphContractRegistrySettingsApplySanitizerTest.php` (preserve `@odata.type`, strip ids)
+- Feature: `tests/Feature/Filament/SettingsCatalogRestoreApplySettingsTest.php` (mock Graph, assert POST body includes `@odata.type` and success/failure flows)
+- Feature: add a restore test that simulates a settings apply route-missing error and verifies fallback policy creation + new policy id recorded, including metadata-only retry when create returns `NotSupported`.
+
+### Verification
+- `./vendor/bin/pest tests/Unit/GraphContractRegistrySettingsApplySanitizerTest.php`
+- `./vendor/bin/pest tests/Feature/Filament/SettingsCatalogRestoreApplySettingsTest.php`
+- `./vendor/bin/pint --dirty`
+
+### Acceptance Criteria
+- RestoreRun for `settingsCatalogPolicy` no longer fails with `choiceSettingValue does not exist …` when Graph supports settings POST.
+- POST `.../settings` includes `settingInstance.@odata.type` (recursive) and is accepted by Graph, or the restore item is marked `manual_required` with request IDs visible.
+- No regressions for other restore types.
+- 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
+
+- [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`.
+- [x] 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.
+
+
+## Phase 7: User Story 5 - Operational readiness and environments (Priority: P2)
+
+- [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: 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.
+
+- [x] T030 [TENANT] Migration für `tenants` ergänzen/prüfen (name, tenant_id, domain, app_client_id, app_status, app_notes, timestamps).
+- [x] T031 [TENANT] Eloquent Model `Tenant` (Beziehungen, tenant-aware scopes).
+- [x] T032 [TENANT] Filament `TenantResource` (list/create/edit/detail; Actions: Open in Entra, Copy consent URL optional).
+- [x] T033 [TENANT] `TenantConfigService` / Graph connectivity check.
+- [x] T034 [TENANT] Action „Verify configuration“ + Audit (`tenant.config.verified`).
+- [x] T035 [TENANT] Tenant-Kontext in Policy/Backup/Restore/Audit Services (tenant_id überall setzen).
+- [x] T036 [TENANT] Feature-Test `TenantSetupTest` (ok/error + Audit).
+- [x] T037 [TENANT] Admin-Consent Callback Route (state → tenant mapping, status update, UI page).
+
+## Phase 9: User Story 6 - Berechtigungsübersicht & Health-Status (Priority: P1)
+
+- [x] T040 [P] [US6] Zentrale Permissions-Liste `config/intune_permissions.php` (+ optional `docs/permissions.md`).
+- [x] T041 [US6] Datenmodell Tenant-Berechtigungen (JSONB `granted_permissions` oder `tenant_permissions` Tabelle; status ok/missing/error).
+- [x] T042 [US6] `TenantPermissionService` (required, granted, compare DTO).
+- [x] T043 [US6] Tenant-Detail UI Panel „Permissions“ (required list + status).
+- [x] T044 [US6] Verify erweitern: compare + persist + Audit `tenant.permissions.checked`.
+- [x] T045 [US6] Tests: Unit compare + Feature Tenant detail status + Verify updates.
+
+## Phase 9b: Scope-Ausrichtung auf neue Objekttypen
+
+- [x] T028 [Scope] `config/tenantpilot.php` auf `scope.supported_types` erweitern; single source for sync/backup/restore.
+- [x] T029 [Scope] Filament-UI an neue Typen anpassen (Filter/Grouping + Restore-Level Hinweise).
+
+## Phase 10: Housekeeping – Delete-Funktionen für Backups & Versions
+
+- [x] T060 [HK] BackupSets soft deletable + Guard gegen RestoreRuns.
+- [x] T061 [HK] Filament Delete BackupSets + Confirmation + Audit (`backup.deleted`) + Guard.
+- [x] T062 [HK] PolicyVersions soft deletable + Queries/Resources default non-deleted.
+- [x] T063 [HK] Filament Delete PolicyVersions + Audit (`policy_version.deleted`).
+- [x] T064 [HK] Tests Housekeeping (BackupSet delete ok/block + PolicyVersion delete).
+
+## Phase 11: Housekeeping – Tenant löschen/deaktivieren
+
+- [x] T070 [HK] Tenants soft deletable (optional status active/archived).
+- [x] T071 [HK] Tenant deactivate/archive action + Audit (`tenant.archived`).
+- [x] T072 [HK] Block operations for deactivated tenants (sync/backup/restore early fail).
+- [x] T073 [HK] RestoreRuns soft deletable (optional) + Audit (`restore_run.deleted`).
+- [x] T074 [HK] Tests Tenant delete/deactivate behavior + clear errors, no Graph calls.
+
+## Phase 12: Housekeeping – Hard Deletes (Force Delete)
+
+- [x] T075 [HK] Force-Delete-Actions (only in trashed; guards; audit before delete) + tests.
+
+## Phase 12b: Single current tenant ("Highlander")
+
+- [x] T120 [TENANT] Migration add `is_current` + partial unique index.
+- [x] T121 [TENANT] Tenant::current() + makeCurrent() + remove implicit defaults.
+- [x] T122 [TENANT] Data cleanup (mark one current; archive local-tenant).
+- [x] T123 [TENANT] Filament UI badge + “Make current” action.
+- [x] T124 [TENANT] Consumers refactor to `Tenant::current()` or explicit tenant.
+- [x] T125 [TENANT] Tests for current selection + “Make current”.
+
+- [x] T130 [UX] Tabellen-Aktionen in Dropdown bündeln (ActionGroup) in TenantResource (+ optional others).
+
+## Phase 13: Settings Normalization & Display (Priority: P1)
+
+- [x] T140 [P] [US1b] Unit test PolicyNormalizer.
+- [x] T141 [P] [US1b] Feature test Policy Settings section.
+- [x] T142 [P] [US1b] Feature test Version detail pretty JSON + normalized.
+- [x] T143 [P] [Edge] Feature test malformed snapshot warning.
+- [x] T144 [P] [Edge] Feature test @odata.type mismatch flag + restore exec block.
+
+- [x] T145 [US1b] PolicyNormalizer service.
+- [x] T146 [US1b] Settings infolist in PolicyResource.
+- [x] T147 [US1b] PolicyVersion view pretty JSON + normalized.
+- [x] T148 [US1b] Integrations (list badge, optional diff enhancements, tenant scoping).
+- [x] T149 [Edge] SnapshotValidator helper.
+- [x] T150 [Edge] @odata.type validator (policy/backup/restore gates).
+- [x] T151 [Edge] UI warnings + restore execution gating (preview may show).
+- [x] T152 [US1b] README docs for settings display.
+- [x] 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).
+
+- [x] 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`.
+- [x] 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`.
+- [x] 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`.
+- [x] 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`.
+
+- [x] 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`.
+
+- [x] 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`.
+- [x] 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.
+- [x] 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`.
+- [x] 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.
+
+- [x] 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.
+- [x] 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`.
+- [x] 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`.
+- [x] 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.
+- [x] 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`.
+- [x] 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.