# Research — Intune RBAC Inventory & Backup v1 (127) ## Decisions ### Decision 1 — Register RBAC as two new foundation types - **Chosen**: Add `intuneRoleDefinition` and `intuneRoleAssignment` to `config/tenantpilot.php` under `foundation_types`, with category `RBAC`, deterministic risk metadata, and `restore: preview-only`. - **Rationale**: Existing foundation support is config-driven. `InventorySyncService`, `BackupService`, `InventoryPolicyTypeMeta`, `BackupItem::isFoundation()`, and Filament coverage/backup surfaces already derive classification and restore/risk semantics from `config('tenantpilot.foundation_types')`. - **Alternatives considered**: - Model RBAC as supported policy types rather than foundations. Rejected because the product and existing architecture already distinguish governance foundations from policy families. - Merge definitions and assignments into a single RBAC type. Rejected because payloads, risks, and future compare/restore semantics differ materially. ### Decision 2 — Use new inventory-grade Graph contract keys for RBAC - **Chosen**: Introduce inventory-grade contract keys named `intuneRoleDefinition` and `intuneRoleAssignment` in `config/graph_contracts.php`, while leaving `directoryRoleDefinitions` and `rbacRoleAssignment` intact for current health/onboarding usage. - **Rationale**: The repo already contains narrowly scoped RBAC keys used by onboarding and health services. `GraphContractRegistry::directoryRoleDefinitionsListPath()` and `GraphContractRegistry::rbacRoleAssignmentListPath()` are referenced by `RbacOnboardingService` and `RbacHealthService`. Reusing those keys for full-fidelity snapshot capture would silently change behavior in existing flows. - **Alternatives considered**: - Expand the existing keys in place. Rejected because it couples inventory fidelity requirements to onboarding and health-check behavior, increasing regression risk. - Hardcode RBAC endpoints in feature services. Rejected because it violates the constitution’s single contract path to Graph. ### Decision 3 — Reuse existing inventory sync persistence through `InventoryItem` - **Chosen**: Keep RBAC inventory in `InventoryItem` rows created by `App\Services\Inventory\InventorySyncService`, using sanitized `meta_jsonb` only. - **Rationale**: Existing foundation inventory already works this way. `InventorySyncService` merges `supported_policy_types` and `foundation_types`, fetches by configured type, and stores a tenant-scoped `InventoryItem` keyed by `tenant_id + policy_type + external_id`. Existing tests already verify this behavior for `roleScopeTag`. - **Alternatives considered**: - Add dedicated RBAC inventory tables. Rejected because the repo already has a generic inventory store and the feature does not require custom query semantics. - Persist full RBAC payloads in inventory. Rejected because the constitution requires metadata-only inventory and immutable payload storage elsewhere. ### Decision 4 — Reuse foundation backup capture for immutable RBAC payloads - **Chosen**: Reuse `App\Services\Intune\FoundationSnapshotService` and `App\Services\Intune\BackupService::captureFoundations()` for RBAC payload capture, but create synthetic tenant-scoped `Policy` anchors plus immutable `PolicyVersion` rows for `intuneRoleDefinition` and `intuneRoleAssignment`. - **Rationale**: RBAC still enters the system through the existing foundation capture path and `include_foundations`, but linked `PolicyVersion` rows let backup-set detail and policy-version detail reuse the same immutable review surface. This preserves foundation semantics without introducing a separate RBAC snapshot model. - **Alternatives considered**: - Keep RBAC backup items permanently detached from `PolicyVersion`. Rejected after implementation because it would leave backup-set detail and version detail on different immutable paths and make readable RBAC history inconsistent with the product goal. - Create a separate RBAC snapshot model. Rejected because it duplicates current foundation backup behavior and would fragment restore/preview surfaces. ### Decision 5 — Add dedicated RBAC normalizers through the existing policy normalizer registry - **Chosen**: Implement `IntuneRoleDefinitionNormalizer` and `IntuneRoleAssignmentNormalizer` as `PolicyTypeNormalizer` implementations and register them through the `policy-type-normalizers` tag in `AppServiceProvider`. - **Rationale**: `PolicyNormalizer` already resolves type-specific normalizers this way. This keeps RBAC readable in version and backup surfaces while preserving the repo’s normalization strategy for future diffing. - **Alternatives considered**: - Use the default normalizer only. Rejected because raw RBAC payloads are not sufficiently readable for governance and audit use. - Add ad-hoc formatting inside Filament screens. Rejected because it would bypass the centralized normalization path and make future diffs inconsistent. ### Decision 6 — Handle missing `DeviceManagementRBAC.Read.All` as a verification warning and capture failure reason - **Chosen**: Reuse existing provider reason and verification cluster patterns so missing RBAC read permission results in a clear warning or non-blocking failure reason rather than a pipeline crash. - **Rationale**: The repo already has soft-permission patterns in `TenantPermissionCheckClusters`, `ProviderReasonCodes`, `RunFailureSanitizer`, and fail-soft Graph helper code such as `ScopeTagResolver`. Existing RBAC health and onboarding services also route reason codes through `ProviderReasonCodes`. - **Alternatives considered**: - Let Graph 403 bubble as a generic exception. Rejected because it produces opaque failures and weak operator guidance. - Mark missing delegated or application permission as hard-blocking for all inventory. Rejected because the spec explicitly requires graceful handling and partial continuity for unrelated inventory types. ### Decision 7 — Preserve preview-only restore by configuration, not special-case UI logic - **Chosen**: Rely on existing restore-mode evaluation through config metadata and current restore-risk checks rather than adding RBAC-only branches. - **Rationale**: `InventoryPolicyTypeMeta`, `RestoreRiskChecker`, `RestoreService`, and badge tests already treat `preview-only` as a first-class mode. Adding the new foundation types with `restore: preview-only` lets existing restore guards and UI labels work consistently. - **Alternatives considered**: - Add RBAC-specific UI suppression logic. Rejected because it is redundant and risks diverging from the canonical restore-mode rules. ### Decision 8 — Treat Entra admin role reporting as a normalization reference, not a storage model - **Chosen**: Reuse payload-shaping ideas from `EntraAdminRolesReportService` for readable role-definition/assignment summaries, but keep Intune RBAC data inside the inventory/foundation architecture. - **Rationale**: The Entra reporting code already demonstrates stable role/assignment mapping and fingerprint-friendly sorting, which is useful for readable RBAC normalization. However, it is a separate feature and not a replacement for tenant inventory or backup storage. - **Alternatives considered**: - Reuse Entra role reporting models directly. Rejected because the domain is different and the feature scope is Intune RBAC foundations, not Entra directory roles. ## Resolved Unknowns - **Unknown**: Whether existing RBAC contract keys could be reused safely. - **Resolved**: No. Existing keys are already consumed by RBAC onboarding and health flows and should stay backward compatible. - **Unknown**: Whether foundations already had an immutable full-payload path. - **Resolved**: Yes. `FoundationSnapshotService` + `BackupService::captureFoundations()` already persists full foundation payloads as `BackupItem` rows. - **Unknown**: Whether inventory stores full payloads or sanitized metadata. - **Resolved**: Inventory stores sanitized metadata only in `InventoryItem.meta_jsonb`; full payloads belong in backup/version artifacts. - **Unknown**: Whether preview-only restore needs a new specialized guard. - **Resolved**: No. Existing config-driven restore-mode evaluation already supports `preview-only` across coverage and restore paths. ## Repo Evidence - Foundation registration baseline: `config/tenantpilot.php` - Existing RBAC contract keys: `config/graph_contracts.php` - Contract helper usage in onboarding and health: `app/Services/Graph/GraphContractRegistry.php`, `app/Services/Intune/RbacOnboardingService.php`, `app/Services/Intune/RbacHealthService.php` - Inventory persistence path: `app/Services/Inventory/InventorySyncService.php` - Coverage payload model: `app/Support/Inventory/InventoryCoverage.php` - Foundation payload backup path: `app/Services/Intune/FoundationSnapshotService.php`, `app/Services/Intune/BackupService.php` - Normalizer registry: `app/Services/Intune/PolicyNormalizer.php`, `app/Providers/AppServiceProvider.php` - Permission-warning patterns: `app/Support/Verification/TenantPermissionCheckClusters.php`, `app/Support/Providers/ProviderReasonCodes.php`, `app/Services/Graph/ScopeTagResolver.php`