TenantAtlas/app/Services/Intune/EnrollmentAutopilotPolicyNormalizer.php
ahmido 817ad208da feat/027-enrollment-config-subtypes (#31)
expose enrollment config subtypes as their own policy types (limit/platform restrictions/notifications) with preview-only restore risk and proper Graph contracts
classify enrollment configs by their @odata.type + deviceEnrollmentConfigurationType so sync only keeps ESP in windowsEnrollmentStatusPage and the rest stay in their own types, including new restore-normalizer UI blocks + warnings
hydrate enrollment notifications: snapshot fetch now downloads each notification template + localized messages, normalized view surfaces template names/subjects/messages, and restore previews keep preview-only behavior
tenant UI tweaks: Tenant list and detail actions moved into an action group; “Open in Entra” re-added in index, and detail now has “Deactivate” + tests covering the new menu layout and actions
tests added/updated for sync, snapshots, restores, normalized settings, tenant UI, plus Pint/test suite run

Co-authored-by: Ahmed Darrazi <ahmeddarrazi@adsmac.local>
Reviewed-on: #31
2026-01-04 13:25:15 +00:00

610 lines
23 KiB
PHP

<?php
namespace App\Services\Intune;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
class EnrollmentAutopilotPolicyNormalizer implements PolicyTypeNormalizer
{
public function __construct(private readonly DefaultPolicyNormalizer $defaultNormalizer) {}
public function supports(string $policyType): bool
{
return in_array($policyType, [
'windowsAutopilotDeploymentProfile',
'windowsEnrollmentStatusPage',
'enrollmentRestriction',
'deviceEnrollmentLimitConfiguration',
'deviceEnrollmentPlatformRestrictionsConfiguration',
'deviceEnrollmentNotificationConfiguration',
], true);
}
/**
* @return array{status: string, settings: array<int, array<string, mixed>>, settings_table?: array<string, mixed>, warnings: array<int, string>}
*/
public function normalize(?array $snapshot, string $policyType, ?string $platform = null): array
{
$snapshot = is_array($snapshot) ? $snapshot : [];
$displayName = Arr::get($snapshot, 'displayName') ?? Arr::get($snapshot, 'name');
$description = Arr::get($snapshot, 'description');
$warnings = [];
if ($policyType === 'enrollmentRestriction') {
$warnings[] = 'Restore is preview-only for Enrollment Restrictions.';
}
if ($policyType === 'deviceEnrollmentLimitConfiguration') {
$warnings[] = 'Restore is preview-only for Enrollment Limits.';
}
if ($policyType === 'deviceEnrollmentPlatformRestrictionsConfiguration') {
$warnings[] = 'Restore is preview-only for Platform Restrictions.';
}
if ($policyType === 'deviceEnrollmentNotificationConfiguration') {
$warnings[] = 'Restore is preview-only for Enrollment Notifications.';
}
$generalEntries = [
['key' => 'Type', 'value' => $policyType],
];
if (is_string($displayName) && $displayName !== '') {
$generalEntries[] = ['key' => 'Display name', 'value' => $displayName];
}
if (is_string($description) && $description !== '') {
$generalEntries[] = ['key' => 'Description', 'value' => $description];
}
$odataType = Arr::get($snapshot, '@odata.type');
if (is_string($odataType) && $odataType !== '') {
$generalEntries[] = ['key' => '@odata.type', 'value' => $odataType];
}
$roleScopeTagIds = Arr::get($snapshot, 'roleScopeTagIds');
if (is_array($roleScopeTagIds) && $roleScopeTagIds !== []) {
$generalEntries[] = ['key' => 'Scope tag IDs', 'value' => array_values($roleScopeTagIds)];
}
$settings = [
[
'type' => 'keyValue',
'title' => 'General',
'entries' => $generalEntries,
],
];
$typeBlock = match ($policyType) {
'windowsAutopilotDeploymentProfile' => $this->buildAutopilotBlock($snapshot),
'windowsEnrollmentStatusPage' => $this->buildEnrollmentStatusPageBlock($snapshot),
'enrollmentRestriction' => $this->buildEnrollmentRestrictionBlock($snapshot),
'deviceEnrollmentLimitConfiguration' => $this->buildEnrollmentLimitBlock($snapshot),
'deviceEnrollmentPlatformRestrictionsConfiguration' => $this->buildEnrollmentPlatformRestrictionsBlock($snapshot),
'deviceEnrollmentNotificationConfiguration' => $this->buildEnrollmentNotificationBlock($snapshot),
default => null,
};
if ($typeBlock !== null) {
$settings[] = $typeBlock;
}
$settings = array_values(array_filter($settings));
return [
'status' => 'ok',
'settings' => $settings,
'warnings' => $warnings,
];
}
/**
* @return array{type: string, title: string, entries: array<int, array{key: string, value: mixed}>}|null
*/
private function buildAutopilotBlock(array $snapshot): ?array
{
$entries = [];
foreach ([
'deviceNameTemplate' => 'Device name template',
'language' => 'Language',
'locale' => 'Locale',
'deploymentMode' => 'Deployment mode',
'deviceType' => 'Device type',
'enableWhiteGlove' => 'Pre-provisioning (White Glove)',
'hybridAzureADJoinSkipConnectivityCheck' => 'Skip Hybrid AAD connectivity check',
] as $key => $label) {
$value = Arr::get($snapshot, $key);
if (is_string($value) && $value !== '') {
$entries[] = ['key' => $label, 'value' => $value];
} elseif (is_bool($value)) {
$entries[] = ['key' => $label, 'value' => $value ? 'Enabled' : 'Disabled'];
}
}
$oobe = Arr::get($snapshot, 'outOfBoxExperienceSettings');
if (is_array($oobe) && $oobe !== []) {
$oobe = Arr::except($oobe, ['@odata.type']);
foreach ($this->expandOutOfBoxExperienceEntries($oobe) as $entry) {
$entries[] = $entry;
}
}
$assignments = Arr::get($snapshot, 'assignments');
if (is_array($assignments) && $assignments !== []) {
$entries[] = ['key' => 'Assignments (snapshot)', 'value' => '[present]'];
}
if ($entries === []) {
return null;
}
return [
'type' => 'keyValue',
'title' => 'Autopilot profile',
'entries' => $entries,
];
}
/**
* @return array<int, array{key: string, value: mixed}>
*/
private function expandOutOfBoxExperienceEntries(array $oobe): array
{
$knownKeys = [
'hideEULA' => 'Hide EULA',
'userType' => 'User type',
'hideEscapeLink' => 'Hide escape link',
'deviceUsageType' => 'Device usage type',
'hidePrivacySettings' => 'Hide privacy settings',
'skipKeyboardSelectionPage' => 'Skip keyboard selection page',
'skipExpressSettings' => 'Skip express settings',
];
$entries = [];
foreach ($knownKeys as $key => $label) {
if (! array_key_exists($key, $oobe)) {
continue;
}
$value = $oobe[$key];
if (is_bool($value)) {
$entries[] = ['key' => "OOBE: {$label}", 'value' => $value ? 'Enabled' : 'Disabled'];
} elseif (is_string($value) && $value !== '') {
$entries[] = ['key' => "OOBE: {$label}", 'value' => $value];
} elseif (is_int($value) || is_float($value)) {
$entries[] = ['key' => "OOBE: {$label}", 'value' => $value];
}
unset($oobe[$key]);
}
foreach ($oobe as $key => $value) {
$label = Str::headline((string) $key);
if (is_bool($value)) {
$entries[] = ['key' => "OOBE: {$label}", 'value' => $value ? 'Enabled' : 'Disabled'];
} elseif (is_string($value) && $value !== '') {
$entries[] = ['key' => "OOBE: {$label}", 'value' => $value];
} elseif (is_int($value) || is_float($value)) {
$entries[] = ['key' => "OOBE: {$label}", 'value' => $value];
} elseif (is_array($value) && $value !== []) {
$entries[] = ['key' => "OOBE: {$label}", 'value' => $value];
}
}
return $entries;
}
/**
* @return array{type: string, title: string, entries: array<int, array{key: string, value: mixed}>}|null
*/
private function buildEnrollmentStatusPageBlock(array $snapshot): ?array
{
$entries = [];
foreach ([
'priority' => 'Priority',
'showInstallationProgress' => 'Show installation progress',
'blockDeviceSetupRetryByUser' => 'Block retry by user',
'allowDeviceResetOnInstallFailure' => 'Allow device reset on install failure',
'installProgressTimeoutInMinutes' => 'Install progress timeout (minutes)',
'allowLogCollectionOnInstallFailure' => 'Allow log collection on failure',
] as $key => $label) {
$value = Arr::get($snapshot, $key);
if (is_int($value) || is_float($value)) {
$entries[] = ['key' => $label, 'value' => $value];
} elseif (is_string($value) && $value !== '') {
$entries[] = ['key' => $label, 'value' => $value];
} elseif (is_bool($value)) {
$entries[] = ['key' => $label, 'value' => $value ? 'Enabled' : 'Disabled'];
}
}
$selected = Arr::get($snapshot, 'selectedMobileAppIds');
if (is_array($selected) && $selected !== []) {
$entries[] = ['key' => 'Selected mobile app IDs', 'value' => array_values($selected)];
}
$assigned = Arr::get($snapshot, 'assignments');
if (is_array($assigned) && $assigned !== []) {
$entries[] = ['key' => 'Assignments (snapshot)', 'value' => '[present]'];
}
if ($entries === []) {
return null;
}
return [
'type' => 'keyValue',
'title' => 'Enrollment Status Page (ESP)',
'entries' => $entries,
];
}
/**
* @return array{type: string, title: string, entries: array<int, array{key: string, value: mixed}>}|null
*/
private function buildEnrollmentRestrictionBlock(array $snapshot): ?array
{
$entries = [];
foreach ([
'priority' => 'Priority',
'version' => 'Version',
'deviceEnrollmentConfigurationType' => 'Configuration type',
] as $key => $label) {
$value = Arr::get($snapshot, $key);
if (is_int($value) || is_float($value)) {
$entries[] = ['key' => $label, 'value' => $value];
} elseif (is_string($value) && $value !== '') {
$entries[] = ['key' => $label, 'value' => $value];
}
}
$platformRestrictions = Arr::get($snapshot, 'platformRestrictions');
$platformRestriction = Arr::get($snapshot, 'platformRestriction');
$platformPayload = is_array($platformRestrictions) && $platformRestrictions !== []
? $platformRestrictions
: (is_array($platformRestriction) ? $platformRestriction : null);
if (is_array($platformPayload) && $platformPayload !== []) {
$platformPayload = Arr::except($platformPayload, ['@odata.type']);
$platformBlocked = Arr::get($platformPayload, 'platformBlocked');
if (is_bool($platformBlocked)) {
$entries[] = ['key' => 'Platform blocked', 'value' => $platformBlocked ? 'Enabled' : 'Disabled'];
}
$personalBlocked = Arr::get($platformPayload, 'personalDeviceEnrollmentBlocked');
if (is_bool($personalBlocked)) {
$entries[] = ['key' => 'Personal device enrollment blocked', 'value' => $personalBlocked ? 'Enabled' : 'Disabled'];
}
$osMin = Arr::get($platformPayload, 'osMinimumVersion');
$entries[] = [
'key' => 'OS minimum version',
'value' => (is_string($osMin) && $osMin !== '') ? $osMin : 'None',
];
$osMax = Arr::get($platformPayload, 'osMaximumVersion');
$entries[] = [
'key' => 'OS maximum version',
'value' => (is_string($osMax) && $osMax !== '') ? $osMax : 'None',
];
$blockedManufacturers = Arr::get($platformPayload, 'blockedManufacturers');
$entries[] = [
'key' => 'Blocked manufacturers',
'value' => (is_array($blockedManufacturers) && $blockedManufacturers !== [])
? array_values($blockedManufacturers)
: ['None'],
];
$blockedSkus = Arr::get($platformPayload, 'blockedSkus');
$entries[] = [
'key' => 'Blocked SKUs',
'value' => (is_array($blockedSkus) && $blockedSkus !== [])
? array_values($blockedSkus)
: ['None'],
];
}
$assigned = Arr::get($snapshot, 'assignments');
if (is_array($assigned) && $assigned !== []) {
$entries[] = ['key' => 'Assignments (snapshot)', 'value' => '[present]'];
}
if ($entries === []) {
return null;
}
return [
'type' => 'keyValue',
'title' => 'Enrollment restrictions',
'entries' => $entries,
];
}
/**
* @return array{type: string, title: string, entries: array<int, array{key: string, value: mixed}>}|null
*/
private function buildEnrollmentLimitBlock(array $snapshot): ?array
{
$entries = [];
foreach ([
'priority' => 'Priority',
'version' => 'Version',
'deviceEnrollmentConfigurationType' => 'Configuration type',
'limit' => 'Enrollment limit',
'limitType' => 'Limit type',
] as $key => $label) {
$value = Arr::get($snapshot, $key);
if (is_int($value) || is_float($value)) {
$entries[] = ['key' => $label, 'value' => $value];
} elseif (is_string($value) && $value !== '') {
$entries[] = ['key' => $label, 'value' => $value];
} elseif (is_bool($value)) {
$entries[] = ['key' => $label, 'value' => $value ? 'Enabled' : 'Disabled'];
}
}
$assigned = Arr::get($snapshot, 'assignments');
if (is_array($assigned) && $assigned !== []) {
$entries[] = ['key' => 'Assignments (snapshot)', 'value' => '[present]'];
}
if ($entries === []) {
return null;
}
return [
'type' => 'keyValue',
'title' => 'Enrollment limits',
'entries' => $entries,
];
}
/**
* @return array{type: string, title: string, entries: array<int, array{key: string, value: mixed}>}|null
*/
private function buildEnrollmentPlatformRestrictionsBlock(array $snapshot): ?array
{
$entries = [];
foreach ([
'priority' => 'Priority',
'version' => 'Version',
'platformType' => 'Platform type',
'deviceEnrollmentConfigurationType' => 'Configuration type',
] as $key => $label) {
$value = Arr::get($snapshot, $key);
if (is_int($value) || is_float($value)) {
$entries[] = ['key' => $label, 'value' => $value];
} elseif (is_string($value) && $value !== '') {
$entries[] = ['key' => $label, 'value' => $value];
}
}
$platformPayload = Arr::get($snapshot, 'platformRestrictions') ?? Arr::get($snapshot, 'platformRestriction');
if (is_array($platformPayload) && $platformPayload !== []) {
$prefix = (string) (Arr::get($snapshot, 'platformType') ?: 'Platform');
$this->appendPlatformRestrictionEntries($entries, $prefix, $platformPayload);
}
$typedRestrictions = [
'androidForWorkRestriction' => 'Android work profile',
'androidRestriction' => 'Android',
'iosRestriction' => 'iOS/iPadOS',
'macRestriction' => 'macOS',
'windowsRestriction' => 'Windows',
];
foreach ($typedRestrictions as $key => $prefix) {
$restriction = Arr::get($snapshot, $key);
if (! is_array($restriction) || $restriction === []) {
continue;
}
$this->appendPlatformRestrictionEntries($entries, $prefix, $restriction);
}
$assigned = Arr::get($snapshot, 'assignments');
if (is_array($assigned) && $assigned !== []) {
$entries[] = ['key' => 'Assignments (snapshot)', 'value' => '[present]'];
}
if ($entries === []) {
return null;
}
return [
'type' => 'keyValue',
'title' => 'Platform restrictions (enrollment)',
'entries' => $entries,
];
}
/**
* @param array<int, array{key: string, value: mixed}> $entries
*/
private function appendPlatformRestrictionEntries(array &$entries, string $prefix, array $payload): void
{
$payload = Arr::except($payload, ['@odata.type']);
$platformBlocked = Arr::get($payload, 'platformBlocked');
if (is_bool($platformBlocked)) {
$entries[] = ['key' => "{$prefix}: Platform blocked", 'value' => $platformBlocked ? 'Enabled' : 'Disabled'];
}
$personalBlocked = Arr::get($payload, 'personalDeviceEnrollmentBlocked');
if (is_bool($personalBlocked)) {
$entries[] = ['key' => "{$prefix}: Personal device enrollment blocked", 'value' => $personalBlocked ? 'Enabled' : 'Disabled'];
}
$osMin = Arr::get($payload, 'osMinimumVersion');
$entries[] = [
'key' => "{$prefix}: OS minimum version",
'value' => (is_string($osMin) && $osMin !== '') ? $osMin : 'None',
];
$osMax = Arr::get($payload, 'osMaximumVersion');
$entries[] = [
'key' => "{$prefix}: OS maximum version",
'value' => (is_string($osMax) && $osMax !== '') ? $osMax : 'None',
];
$blockedManufacturers = Arr::get($payload, 'blockedManufacturers');
$entries[] = [
'key' => "{$prefix}: Blocked manufacturers",
'value' => (is_array($blockedManufacturers) && $blockedManufacturers !== [])
? array_values($blockedManufacturers)
: ['None'],
];
$blockedSkus = Arr::get($payload, 'blockedSkus');
$entries[] = [
'key' => "{$prefix}: Blocked SKUs",
'value' => (is_array($blockedSkus) && $blockedSkus !== [])
? array_values($blockedSkus)
: ['None'],
];
}
/**
* @return array{type: string, title: string, entries: array<int, array{key: string, value: mixed}>}|null
*/
private function buildEnrollmentNotificationBlock(array $snapshot): ?array
{
$entries = [];
foreach ([
'priority' => 'Priority',
'version' => 'Version',
'platformType' => 'Platform type',
'deviceEnrollmentConfigurationType' => 'Configuration type',
'brandingOptions' => 'Branding options',
'templateType' => 'Template type',
'defaultLocale' => 'Default locale',
'notificationMessageTemplateId' => 'Notification message template ID',
] as $key => $label) {
$value = Arr::get($snapshot, $key);
if (is_int($value) || is_float($value)) {
$entries[] = ['key' => $label, 'value' => $value];
} elseif (is_string($value) && $value !== '') {
$entries[] = ['key' => $label, 'value' => $value];
} elseif (is_bool($value)) {
$entries[] = ['key' => $label, 'value' => $value ? 'Enabled' : 'Disabled'];
}
}
$notificationTemplates = Arr::get($snapshot, 'notificationTemplates');
if (is_array($notificationTemplates) && $notificationTemplates !== []) {
$entries[] = ['key' => 'Notification templates', 'value' => array_values($notificationTemplates)];
}
$templateSnapshots = Arr::get($snapshot, 'notificationTemplateSnapshots');
if (is_array($templateSnapshots) && $templateSnapshots !== []) {
foreach ($templateSnapshots as $templateSnapshot) {
if (! is_array($templateSnapshot)) {
continue;
}
$channel = Arr::get($templateSnapshot, 'channel');
$channelLabel = is_string($channel) && $channel !== '' ? $channel : 'Template';
$templateId = Arr::get($templateSnapshot, 'template_id');
if (is_string($templateId) && $templateId !== '') {
$entries[] = ['key' => "{$channelLabel} template ID", 'value' => $templateId];
}
$template = Arr::get($templateSnapshot, 'template');
if (is_array($template) && $template !== []) {
$displayName = Arr::get($template, 'displayName');
if (is_string($displayName) && $displayName !== '') {
$entries[] = ['key' => "{$channelLabel} template name", 'value' => $displayName];
}
$brandingOptions = Arr::get($template, 'brandingOptions');
if (is_string($brandingOptions) && $brandingOptions !== '') {
$entries[] = ['key' => "{$channelLabel} branding options", 'value' => $brandingOptions];
}
$defaultLocale = Arr::get($template, 'defaultLocale');
if (is_string($defaultLocale) && $defaultLocale !== '') {
$entries[] = ['key' => "{$channelLabel} default locale", 'value' => $defaultLocale];
}
}
$localizedMessages = Arr::get($templateSnapshot, 'localized_notification_messages');
if (is_array($localizedMessages) && $localizedMessages !== []) {
foreach ($localizedMessages as $localizedMessage) {
if (! is_array($localizedMessage)) {
continue;
}
$locale = Arr::get($localizedMessage, 'locale');
$localeLabel = is_string($locale) && $locale !== '' ? $locale : 'locale';
$subject = Arr::get($localizedMessage, 'subject');
if (is_string($subject) && $subject !== '') {
$entries[] = ['key' => "{$channelLabel} ({$localeLabel}) Subject", 'value' => $subject];
}
$messageTemplate = Arr::get($localizedMessage, 'messageTemplate');
if (is_string($messageTemplate) && $messageTemplate !== '') {
$entries[] = ['key' => "{$channelLabel} ({$localeLabel}) Message", 'value' => $messageTemplate];
}
$isDefault = Arr::get($localizedMessage, 'isDefault');
if (is_bool($isDefault)) {
$entries[] = ['key' => "{$channelLabel} ({$localeLabel}) Default", 'value' => $isDefault ? 'Enabled' : 'Disabled'];
}
}
}
}
}
$assigned = Arr::get($snapshot, 'assignments');
if (is_array($assigned) && $assigned !== []) {
$entries[] = ['key' => 'Assignments (snapshot)', 'value' => '[present]'];
}
if ($entries === []) {
return null;
}
return [
'type' => 'keyValue',
'title' => 'Enrollment notifications',
'entries' => $entries,
];
}
/**
* @return array<string, mixed>
*/
public function flattenForDiff(?array $snapshot, string $policyType, ?string $platform = null): array
{
$normalized = $this->normalize($snapshot ?? [], $policyType, $platform);
return $this->defaultNormalizer->flattenNormalizedForDiff($normalized);
}
}