# Feature Specification: TenantPilot v1

**Feature Branch**: `tenantpilot-v1`  
**Created**: 2025-12-10  
**Status**: Draft  
**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."
  supported_types:
    - key: deviceConfiguration
      name: "Device Configuration"
      graph_resource: "deviceManagement/deviceConfigurations"
      notes: "Inklusive Custom OMA-URI, Administrative Templates und Settings Catalog."

    - 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: 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: 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"
      filter: "odata.type eq '#microsoft.graph.windows10EnrollmentCompletionPageConfiguration'"

    - key: endpointSecurityIntent
      name: "Endpoint Security Intents"
      graph_resource: "deviceManagement/intents"
      notes: "Account Protection, Disk Encryption etc.; Zuordnung über bekannte Templates."

    - key: mobileApp
      name: "Applications (Metadata only)"
      graph_resource: "deviceAppManagement/mobileApps"
      notes: "Backup nur von Metadaten/Zuweisungen (kein Binary-Download in v1)."
    
    - key: settingsCatalogPolicy
      name: "Settings Catalog Policy"
      graph_resource: "deviceManagement/configurationPolicies"
      notes: "Intune Settings Catalog Policies liegen NICHT unter deviceConfigurations, sondern unter configurationPolicies. v1 behandelt sie als eigenen Typ."

  restore_matrix:
    deviceConfiguration:
      backup: full
      restore: enabled
      risk: medium
      notes: "Standard-Case für Backup+Restore; starke Preview/Audit Pflicht."

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

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

    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."
    
    settingsCatalogPolicy:
      backup: full
      restore: enabled
      risk: medium
      notes: "Settings Catalog Policies sind Standard-Config-Policies (Settings Catalog). Preview/Audit Pflicht; Restore automatisierbar."

    mobileApp:
      backup: metadata-only
      restore: enabled
      risk: low-medium
      notes: "Nur Metadaten/Zuweisungen; kein Binary; Restore setzt Konfigurationen/Zuweisungen wieder."
```

## 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 4b - Rerun a restore operation (Priority: P2)

Admin can rerun a previous restore operation to re-apply the same set of policies with the same settings.

**Why this priority**: If a restore operation fails partially, or if an admin wants to re-apply a known good configuration, a rerun option provides a quick and safe way to do so without re-selecting all policies manually.

**Independent Test**: From the Restore Run detail page, trigger the "Rerun" action. Verify that a new Restore Run is created with the same settings and items as the original run.

**Acceptance Scenarios**:

1. **Given** a completed or failed Restore Run, **When** the admin triggers the "Rerun" action, **Then** a new Restore Run is created with the same `backup_set_id`, selected items, and `dry_run` flag as the original run.
2. **Given** a new Restore Run created via the "Rerun" action, **When** the admin executes it, **Then** it follows the same safety gates and confirmation steps as a manually created Restore Run.
3. **Given** a "Rerun" action is triggered, **Then** an audit event `restore_run.rerun_created` is logged with a reference to the original `restore_run_id`.
---

### 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.
	•	FR-035: System MUST provide a "Rerun" action for any completed or failed restore run, which creates a new restore run pre-populated with the same backup set, selected items, and dry-run flag as the original run. The rerun MUST follow the same safety and confirmation gates.

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

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