This PR completes Feature 014 (Enrollment & Autopilot). Adds normalization for: Autopilot deployment profiles (windowsAutopilotDeploymentProfile) Enrollment Status Page / ESP (windowsEnrollmentStatusPage) Enrollment Restrictions (enrollmentRestriction, restore remains preview-only) Improves settings readability: Autopilot OOBE settings are expanded into readable key/value entries Enrollment restriction platform restrictions are shown as explicit fields (with sensible defaults) Array/list values render as badges (avoids Blade rendering crashes on non-string values) Fixes enrollment configuration type collisions during sync: Canonical type resolution prevents enrollmentRestriction from “claiming” ESP items Safe reclassification updates existing wrong rows instead of skipping Enhances reclassification command: Can detect ESP even if a policy has no local versions (fetches snapshot from Graph) Dry-run by default; apply with --write Tests Added/updated unit + Filament feature tests for normalization and UI rendering. Preview-only enforcement for enrollment restrictions is covered. Targeted test suite and Pint are green. Co-authored-by: Ahmed Darrazi <ahmeddarrazi@adsmac.local> Reviewed-on: #20
332 lines
12 KiB
PHP
332 lines
12 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',
|
|
], 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.';
|
|
}
|
|
|
|
$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),
|
|
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<string, mixed>
|
|
*/
|
|
public function flattenForDiff(?array $snapshot, string $policyType, ?string $platform = null): array
|
|
{
|
|
$normalized = $this->normalize($snapshot ?? [], $policyType, $platform);
|
|
|
|
return $this->defaultNormalizer->flattenNormalizedForDiff($normalized);
|
|
}
|
|
}
|