feat/027-enrollment-config-subtypes #31
@ -157,6 +157,12 @@ public static function table(Table $table): Table
|
|||||||
->url(fn (Tenant $record) => static::adminConsentUrl($record))
|
->url(fn (Tenant $record) => static::adminConsentUrl($record))
|
||||||
->visible(fn (Tenant $record) => static::adminConsentUrl($record) !== null)
|
->visible(fn (Tenant $record) => static::adminConsentUrl($record) !== null)
|
||||||
->openUrlInNewTab(),
|
->openUrlInNewTab(),
|
||||||
|
Actions\Action::make('open_in_entra')
|
||||||
|
->label('Open in Entra')
|
||||||
|
->icon('heroicon-o-arrow-top-right-on-square')
|
||||||
|
->url(fn (Tenant $record) => static::entraUrl($record))
|
||||||
|
->visible(fn (Tenant $record) => static::entraUrl($record) !== null)
|
||||||
|
->openUrlInNewTab(),
|
||||||
Actions\Action::make('verify')
|
Actions\Action::make('verify')
|
||||||
->label('Verify configuration')
|
->label('Verify configuration')
|
||||||
->icon('heroicon-o-check-badge')
|
->icon('heroicon-o-check-badge')
|
||||||
|
|||||||
@ -9,6 +9,7 @@
|
|||||||
use App\Services\Intune\TenantConfigService;
|
use App\Services\Intune\TenantConfigService;
|
||||||
use App\Services\Intune\TenantPermissionService;
|
use App\Services\Intune\TenantPermissionService;
|
||||||
use Filament\Actions;
|
use Filament\Actions;
|
||||||
|
use Filament\Notifications\Notification;
|
||||||
use Filament\Resources\Pages\ViewRecord;
|
use Filament\Resources\Pages\ViewRecord;
|
||||||
|
|
||||||
class ViewTenant extends ViewRecord
|
class ViewTenant extends ViewRecord
|
||||||
@ -18,6 +19,7 @@ class ViewTenant extends ViewRecord
|
|||||||
protected function getHeaderActions(): array
|
protected function getHeaderActions(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
|
Actions\ActionGroup::make([
|
||||||
Actions\EditAction::make(),
|
Actions\EditAction::make(),
|
||||||
Actions\Action::make('admin_consent')
|
Actions\Action::make('admin_consent')
|
||||||
->label('Admin consent')
|
->label('Admin consent')
|
||||||
@ -46,6 +48,34 @@ protected function getHeaderActions(): array
|
|||||||
TenantResource::verifyTenant($record, $configService, $permissionService, $rbacHealthService, $auditLogger);
|
TenantResource::verifyTenant($record, $configService, $permissionService, $rbacHealthService, $auditLogger);
|
||||||
}),
|
}),
|
||||||
TenantResource::rbacAction(),
|
TenantResource::rbacAction(),
|
||||||
|
Actions\Action::make('archive')
|
||||||
|
->label('Deactivate')
|
||||||
|
->color('danger')
|
||||||
|
->icon('heroicon-o-archive-box-x-mark')
|
||||||
|
->requiresConfirmation()
|
||||||
|
->visible(fn (Tenant $record) => ! $record->trashed())
|
||||||
|
->action(function (Tenant $record, AuditLogger $auditLogger) {
|
||||||
|
$record->delete();
|
||||||
|
|
||||||
|
$auditLogger->log(
|
||||||
|
tenant: $record,
|
||||||
|
action: 'tenant.archived',
|
||||||
|
resourceType: 'tenant',
|
||||||
|
resourceId: (string) $record->id,
|
||||||
|
status: 'success',
|
||||||
|
context: ['metadata' => ['tenant_id' => $record->tenant_id]]
|
||||||
|
);
|
||||||
|
|
||||||
|
Notification::make()
|
||||||
|
->title('Tenant deactivated')
|
||||||
|
->body('The tenant has been archived and hidden from lists.')
|
||||||
|
->success()
|
||||||
|
->send();
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
->label('Actions')
|
||||||
|
->icon('heroicon-o-ellipsis-vertical')
|
||||||
|
->color('gray'),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,6 +15,9 @@ public function supports(string $policyType): bool
|
|||||||
'windowsAutopilotDeploymentProfile',
|
'windowsAutopilotDeploymentProfile',
|
||||||
'windowsEnrollmentStatusPage',
|
'windowsEnrollmentStatusPage',
|
||||||
'enrollmentRestriction',
|
'enrollmentRestriction',
|
||||||
|
'deviceEnrollmentLimitConfiguration',
|
||||||
|
'deviceEnrollmentPlatformRestrictionsConfiguration',
|
||||||
|
'deviceEnrollmentNotificationConfiguration',
|
||||||
], true);
|
], true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -34,6 +37,18 @@ public function normalize(?array $snapshot, string $policyType, ?string $platfor
|
|||||||
$warnings[] = 'Restore is preview-only for Enrollment Restrictions.';
|
$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 = [
|
$generalEntries = [
|
||||||
['key' => 'Type', 'value' => $policyType],
|
['key' => 'Type', 'value' => $policyType],
|
||||||
];
|
];
|
||||||
@ -68,6 +83,9 @@ public function normalize(?array $snapshot, string $policyType, ?string $platfor
|
|||||||
'windowsAutopilotDeploymentProfile' => $this->buildAutopilotBlock($snapshot),
|
'windowsAutopilotDeploymentProfile' => $this->buildAutopilotBlock($snapshot),
|
||||||
'windowsEnrollmentStatusPage' => $this->buildEnrollmentStatusPageBlock($snapshot),
|
'windowsEnrollmentStatusPage' => $this->buildEnrollmentStatusPageBlock($snapshot),
|
||||||
'enrollmentRestriction' => $this->buildEnrollmentRestrictionBlock($snapshot),
|
'enrollmentRestriction' => $this->buildEnrollmentRestrictionBlock($snapshot),
|
||||||
|
'deviceEnrollmentLimitConfiguration' => $this->buildEnrollmentLimitBlock($snapshot),
|
||||||
|
'deviceEnrollmentPlatformRestrictionsConfiguration' => $this->buildEnrollmentPlatformRestrictionsBlock($snapshot),
|
||||||
|
'deviceEnrollmentNotificationConfiguration' => $this->buildEnrollmentNotificationBlock($snapshot),
|
||||||
default => null,
|
default => null,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -319,6 +337,266 @@ private function buildEnrollmentRestrictionBlock(array $snapshot): ?array
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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>
|
* @return array<string, mixed>
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -124,6 +124,15 @@ public function fetch(Tenant $tenant, Policy $policy, ?string $actorEmail = null
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($policy->policy_type === 'deviceEnrollmentNotificationConfiguration') {
|
||||||
|
[$payload, $metadata] = $this->hydrateEnrollmentNotificationTemplates(
|
||||||
|
tenantIdentifier: $tenantIdentifier,
|
||||||
|
tenant: $tenant,
|
||||||
|
payload: is_array($payload) ? $payload : [],
|
||||||
|
metadata: $metadata
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if ($response->failed()) {
|
if ($response->failed()) {
|
||||||
$reason = $this->formatGraphFailureReason($response);
|
$reason = $this->formatGraphFailureReason($response);
|
||||||
|
|
||||||
@ -607,6 +616,126 @@ private function hydrateComplianceActions(string $tenantIdentifier, Tenant $tena
|
|||||||
return [$payload, $metadata];
|
return [$payload, $metadata];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hydrate enrollment notifications with message template details.
|
||||||
|
*
|
||||||
|
* @return array{0:array,1:array}
|
||||||
|
*/
|
||||||
|
private function hydrateEnrollmentNotificationTemplates(string $tenantIdentifier, Tenant $tenant, array $payload, array $metadata): array
|
||||||
|
{
|
||||||
|
$existing = $payload['notificationTemplateSnapshots'] ?? null;
|
||||||
|
|
||||||
|
if (is_array($existing) && $existing !== []) {
|
||||||
|
$metadata['enrollment_notification_templates_hydration'] = 'embedded';
|
||||||
|
|
||||||
|
return [$payload, $metadata];
|
||||||
|
}
|
||||||
|
|
||||||
|
$templateRefs = $payload['notificationTemplates'] ?? null;
|
||||||
|
|
||||||
|
if (! is_array($templateRefs) || $templateRefs === []) {
|
||||||
|
$metadata['enrollment_notification_templates_hydration'] = 'none';
|
||||||
|
|
||||||
|
return [$payload, $metadata];
|
||||||
|
}
|
||||||
|
|
||||||
|
$options = [
|
||||||
|
'tenant' => $tenantIdentifier,
|
||||||
|
'client_id' => $tenant->app_client_id,
|
||||||
|
'client_secret' => $tenant->app_client_secret,
|
||||||
|
];
|
||||||
|
|
||||||
|
$snapshots = [];
|
||||||
|
$failures = 0;
|
||||||
|
|
||||||
|
foreach ($templateRefs as $templateRef) {
|
||||||
|
if (! is_string($templateRef) || $templateRef === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
[$channel, $templateId] = $this->parseEnrollmentNotificationTemplateRef($templateRef);
|
||||||
|
|
||||||
|
if ($templateId === null) {
|
||||||
|
$failures++;
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$templatePath = sprintf('deviceManagement/notificationMessageTemplates/%s', urlencode($templateId));
|
||||||
|
$templateResponse = $this->graphClient->request('GET', $templatePath, $options);
|
||||||
|
|
||||||
|
if ($templateResponse->failed() || ! is_array($templateResponse->data)) {
|
||||||
|
$failures++;
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$template = Arr::except($templateResponse->data, ['@odata.context']);
|
||||||
|
|
||||||
|
$messagesPath = sprintf(
|
||||||
|
'deviceManagement/notificationMessageTemplates/%s/localizedNotificationMessages',
|
||||||
|
urlencode($templateId)
|
||||||
|
);
|
||||||
|
$messagesResponse = $this->graphClient->request('GET', $messagesPath, $options);
|
||||||
|
|
||||||
|
$messages = [];
|
||||||
|
|
||||||
|
if ($messagesResponse->failed()) {
|
||||||
|
$failures++;
|
||||||
|
} else {
|
||||||
|
$pageItems = $messagesResponse->data['value'] ?? [];
|
||||||
|
|
||||||
|
if (is_array($pageItems)) {
|
||||||
|
foreach ($pageItems as $message) {
|
||||||
|
if (is_array($message)) {
|
||||||
|
$messages[] = Arr::except($message, ['@odata.context']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$snapshots[] = [
|
||||||
|
'channel' => $channel,
|
||||||
|
'template_id' => $templateId,
|
||||||
|
'template' => $template,
|
||||||
|
'localized_notification_messages' => $messages,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($snapshots === []) {
|
||||||
|
$metadata['enrollment_notification_templates_hydration'] = 'failed';
|
||||||
|
|
||||||
|
return [$payload, $metadata];
|
||||||
|
}
|
||||||
|
|
||||||
|
$payload['notificationTemplateSnapshots'] = $snapshots;
|
||||||
|
|
||||||
|
$metadata['enrollment_notification_templates_hydration'] = $failures > 0 ? 'partial' : 'complete';
|
||||||
|
|
||||||
|
return [$payload, $metadata];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{0:?string,1:?string}
|
||||||
|
*/
|
||||||
|
private function parseEnrollmentNotificationTemplateRef(string $templateRef): array
|
||||||
|
{
|
||||||
|
if (! str_contains($templateRef, '_')) {
|
||||||
|
return [null, $templateRef];
|
||||||
|
}
|
||||||
|
|
||||||
|
[$channel, $templateId] = explode('_', $templateRef, 2);
|
||||||
|
|
||||||
|
$channel = trim($channel);
|
||||||
|
$templateId = trim($templateId);
|
||||||
|
|
||||||
|
if ($templateId === '') {
|
||||||
|
return [$channel !== '' ? $channel : null, null];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [$channel !== '' ? $channel : null, $templateId];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extract all settingDefinitionId from settings array, including nested children.
|
* Extract all settingDefinitionId from settings array, including nested children.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -167,7 +167,15 @@ private function resolveCanonicalPolicyType(string $policyType, array $policyDat
|
|||||||
return $this->resolveConfigurationPolicyType($policyData);
|
return $this->resolveConfigurationPolicyType($policyData);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (! in_array($policyType, ['enrollmentRestriction', 'windowsEnrollmentStatusPage'], true)) {
|
$enrollmentConfigurationTypes = [
|
||||||
|
'enrollmentRestriction',
|
||||||
|
'windowsEnrollmentStatusPage',
|
||||||
|
'deviceEnrollmentLimitConfiguration',
|
||||||
|
'deviceEnrollmentPlatformRestrictionsConfiguration',
|
||||||
|
'deviceEnrollmentNotificationConfiguration',
|
||||||
|
];
|
||||||
|
|
||||||
|
if (! in_array($policyType, $enrollmentConfigurationTypes, true)) {
|
||||||
return $policyType;
|
return $policyType;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -175,6 +183,18 @@ private function resolveCanonicalPolicyType(string $policyType, array $policyDat
|
|||||||
return 'windowsEnrollmentStatusPage';
|
return 'windowsEnrollmentStatusPage';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($this->isEnrollmentNotificationItem($policyData)) {
|
||||||
|
return 'deviceEnrollmentNotificationConfiguration';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->isEnrollmentLimitItem($policyData)) {
|
||||||
|
return 'deviceEnrollmentLimitConfiguration';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->isEnrollmentPlatformRestrictionsItem($policyData)) {
|
||||||
|
return 'deviceEnrollmentPlatformRestrictionsConfiguration';
|
||||||
|
}
|
||||||
|
|
||||||
return 'enrollmentRestriction';
|
return 'enrollmentRestriction';
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -255,13 +275,77 @@ private function isEnrollmentStatusPageItem(array $policyData): bool
|
|||||||
|| (is_string($configurationType) && $configurationType === 'windows10EnrollmentCompletionPageConfiguration');
|
|| (is_string($configurationType) && $configurationType === 'windows10EnrollmentCompletionPageConfiguration');
|
||||||
}
|
}
|
||||||
|
|
||||||
private function reclassifyEnrollmentConfigurationPoliciesIfNeeded(int $tenantId, string $externalId, string $policyType): void
|
private function isEnrollmentLimitItem(array $policyData): bool
|
||||||
{
|
{
|
||||||
if (! in_array($policyType, ['enrollmentRestriction', 'windowsEnrollmentStatusPage'], true)) {
|
$odataType = $policyData['@odata.type'] ?? $policyData['@OData.Type'] ?? null;
|
||||||
return;
|
$configurationType = $policyData['deviceEnrollmentConfigurationType'] ?? null;
|
||||||
|
|
||||||
|
return (is_string($odataType) && strcasecmp($odataType, '#microsoft.graph.deviceEnrollmentLimitConfiguration') === 0)
|
||||||
|
|| (is_string($configurationType) && strcasecmp($configurationType, 'deviceEnrollmentLimitConfiguration') === 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
$enrollmentTypes = ['enrollmentRestriction', 'windowsEnrollmentStatusPage'];
|
private function isEnrollmentPlatformRestrictionsItem(array $policyData): bool
|
||||||
|
{
|
||||||
|
$odataType = $policyData['@odata.type'] ?? $policyData['@OData.Type'] ?? null;
|
||||||
|
$configurationType = $policyData['deviceEnrollmentConfigurationType'] ?? null;
|
||||||
|
|
||||||
|
if (is_string($odataType) && $odataType !== '') {
|
||||||
|
$odataTypeKey = strtolower($odataType);
|
||||||
|
|
||||||
|
if (in_array($odataTypeKey, [
|
||||||
|
'#microsoft.graph.deviceenrollmentplatformrestrictionconfiguration',
|
||||||
|
'#microsoft.graph.deviceenrollmentplatformrestrictionsconfiguration',
|
||||||
|
], true)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_string($configurationType) && $configurationType !== '') {
|
||||||
|
$configurationTypeKey = strtolower($configurationType);
|
||||||
|
|
||||||
|
if (in_array($configurationTypeKey, [
|
||||||
|
'deviceenrollmentplatformrestrictionconfiguration',
|
||||||
|
'deviceenrollmentplatformrestrictionsconfiguration',
|
||||||
|
], true)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function isEnrollmentNotificationItem(array $policyData): bool
|
||||||
|
{
|
||||||
|
$odataType = $policyData['@odata.type'] ?? $policyData['@OData.Type'] ?? null;
|
||||||
|
$configurationType = $policyData['deviceEnrollmentConfigurationType'] ?? null;
|
||||||
|
|
||||||
|
if (is_string($odataType) && strcasecmp($odataType, '#microsoft.graph.deviceEnrollmentNotificationConfiguration') === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! is_string($configurationType) || $configurationType === '') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return in_array(strtolower($configurationType), [
|
||||||
|
'enrollmentnotificationsconfiguration',
|
||||||
|
'deviceenrollmentnotificationconfiguration',
|
||||||
|
], true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function reclassifyEnrollmentConfigurationPoliciesIfNeeded(int $tenantId, string $externalId, string $policyType): void
|
||||||
|
{
|
||||||
|
$enrollmentTypes = [
|
||||||
|
'enrollmentRestriction',
|
||||||
|
'windowsEnrollmentStatusPage',
|
||||||
|
'deviceEnrollmentLimitConfiguration',
|
||||||
|
'deviceEnrollmentPlatformRestrictionsConfiguration',
|
||||||
|
'deviceEnrollmentNotificationConfiguration',
|
||||||
|
];
|
||||||
|
|
||||||
|
if (! in_array($policyType, $enrollmentTypes, true)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
$existingCorrect = Policy::query()
|
$existingCorrect = Policy::query()
|
||||||
->where('tenant_id', $tenantId)
|
->where('tenant_id', $tenantId)
|
||||||
|
|||||||
@ -73,6 +73,15 @@ protected static function odataTypeMap(): array
|
|||||||
'enrollmentRestriction' => [
|
'enrollmentRestriction' => [
|
||||||
'all' => '#microsoft.graph.deviceEnrollmentConfiguration',
|
'all' => '#microsoft.graph.deviceEnrollmentConfiguration',
|
||||||
],
|
],
|
||||||
|
'deviceEnrollmentLimitConfiguration' => [
|
||||||
|
'all' => '#microsoft.graph.deviceEnrollmentLimitConfiguration',
|
||||||
|
],
|
||||||
|
'deviceEnrollmentPlatformRestrictionsConfiguration' => [
|
||||||
|
'all' => '#microsoft.graph.deviceEnrollmentPlatformRestrictionsConfiguration',
|
||||||
|
],
|
||||||
|
'deviceEnrollmentNotificationConfiguration' => [
|
||||||
|
'all' => '#microsoft.graph.deviceEnrollmentNotificationConfiguration',
|
||||||
|
],
|
||||||
'windowsAutopilotDeploymentProfile' => [
|
'windowsAutopilotDeploymentProfile' => [
|
||||||
'windows' => '#microsoft.graph.windowsAutopilotDeploymentProfile',
|
'windows' => '#microsoft.graph.windowsAutopilotDeploymentProfile',
|
||||||
],
|
],
|
||||||
|
|||||||
@ -518,14 +518,29 @@
|
|||||||
'assignments_delete_path' => '/deviceManagement/deviceHealthScripts/{id}/assignments/{assignmentId}',
|
'assignments_delete_path' => '/deviceManagement/deviceHealthScripts/{id}/assignments/{assignmentId}',
|
||||||
'assignments_delete_method' => 'DELETE',
|
'assignments_delete_method' => 'DELETE',
|
||||||
],
|
],
|
||||||
'enrollmentRestriction' => [
|
'deviceEnrollmentLimitConfiguration' => [
|
||||||
|
'resource' => 'deviceManagement/deviceEnrollmentConfigurations',
|
||||||
|
'allowed_select' => ['id', 'displayName', 'description', '@odata.type', 'version'],
|
||||||
|
'allowed_expand' => [],
|
||||||
|
'type_family' => [
|
||||||
|
'#microsoft.graph.deviceEnrollmentLimitConfiguration',
|
||||||
|
],
|
||||||
|
'create_method' => 'POST',
|
||||||
|
'update_method' => 'PATCH',
|
||||||
|
'id_field' => 'id',
|
||||||
|
'hydration' => 'properties',
|
||||||
|
'assignments_list_path' => '/deviceManagement/deviceEnrollmentConfigurations/{id}/assignments',
|
||||||
|
'assignments_create_path' => '/deviceManagement/deviceEnrollmentConfigurations/{id}/assign',
|
||||||
|
'assignments_create_method' => 'POST',
|
||||||
|
'assignments_payload_key' => 'enrollmentConfigurationAssignments',
|
||||||
|
],
|
||||||
|
'deviceEnrollmentPlatformRestrictionsConfiguration' => [
|
||||||
'resource' => 'deviceManagement/deviceEnrollmentConfigurations',
|
'resource' => 'deviceManagement/deviceEnrollmentConfigurations',
|
||||||
'allowed_select' => ['id', 'displayName', 'description', '@odata.type', 'version'],
|
'allowed_select' => ['id', 'displayName', 'description', '@odata.type', 'version'],
|
||||||
'allowed_expand' => [],
|
'allowed_expand' => [],
|
||||||
'type_family' => [
|
'type_family' => [
|
||||||
'#microsoft.graph.deviceEnrollmentPlatformRestrictionConfiguration',
|
'#microsoft.graph.deviceEnrollmentPlatformRestrictionConfiguration',
|
||||||
'#microsoft.graph.deviceEnrollmentPlatformRestrictionsConfiguration',
|
'#microsoft.graph.deviceEnrollmentPlatformRestrictionsConfiguration',
|
||||||
'#microsoft.graph.deviceEnrollmentLimitConfiguration',
|
|
||||||
],
|
],
|
||||||
'create_method' => 'POST',
|
'create_method' => 'POST',
|
||||||
'update_method' => 'PATCH',
|
'update_method' => 'PATCH',
|
||||||
@ -536,6 +551,39 @@
|
|||||||
'assignments_create_method' => 'POST',
|
'assignments_create_method' => 'POST',
|
||||||
'assignments_payload_key' => 'enrollmentConfigurationAssignments',
|
'assignments_payload_key' => 'enrollmentConfigurationAssignments',
|
||||||
],
|
],
|
||||||
|
'deviceEnrollmentNotificationConfiguration' => [
|
||||||
|
'resource' => 'deviceManagement/deviceEnrollmentConfigurations',
|
||||||
|
'allowed_select' => ['id', 'displayName', 'description', '@odata.type', 'version'],
|
||||||
|
'allowed_expand' => [],
|
||||||
|
'type_family' => [
|
||||||
|
'#microsoft.graph.deviceEnrollmentNotificationConfiguration',
|
||||||
|
],
|
||||||
|
'create_method' => 'POST',
|
||||||
|
'update_method' => 'PATCH',
|
||||||
|
'id_field' => 'id',
|
||||||
|
'hydration' => 'properties',
|
||||||
|
'update_strip_keys' => [
|
||||||
|
'notificationTemplateSnapshots',
|
||||||
|
],
|
||||||
|
'assignments_list_path' => '/deviceManagement/deviceEnrollmentConfigurations/{id}/assignments',
|
||||||
|
'assignments_create_path' => '/deviceManagement/deviceEnrollmentConfigurations/{id}/assign',
|
||||||
|
'assignments_create_method' => 'POST',
|
||||||
|
'assignments_payload_key' => 'enrollmentConfigurationAssignments',
|
||||||
|
],
|
||||||
|
'enrollmentRestriction' => [
|
||||||
|
'resource' => 'deviceManagement/deviceEnrollmentConfigurations',
|
||||||
|
'allowed_select' => ['id', 'displayName', 'description', '@odata.type', 'version'],
|
||||||
|
'allowed_expand' => [],
|
||||||
|
'type_family' => [],
|
||||||
|
'create_method' => 'POST',
|
||||||
|
'update_method' => 'PATCH',
|
||||||
|
'id_field' => 'id',
|
||||||
|
'hydration' => 'properties',
|
||||||
|
'assignments_list_path' => '/deviceManagement/deviceEnrollmentConfigurations/{id}/assignments',
|
||||||
|
'assignments_create_path' => '/deviceManagement/deviceEnrollmentConfigurations/{id}/assign',
|
||||||
|
'assignments_create_method' => 'POST',
|
||||||
|
'assignments_payload_key' => 'enrollmentConfigurationAssignments',
|
||||||
|
],
|
||||||
'termsAndConditions' => [
|
'termsAndConditions' => [
|
||||||
'resource' => 'deviceManagement/termsAndConditions',
|
'resource' => 'deviceManagement/termsAndConditions',
|
||||||
'allowed_select' => [
|
'allowed_select' => [
|
||||||
|
|||||||
@ -185,6 +185,37 @@
|
|||||||
'restore' => 'enabled',
|
'restore' => 'enabled',
|
||||||
'risk' => 'medium',
|
'risk' => 'medium',
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
'type' => 'deviceEnrollmentLimitConfiguration',
|
||||||
|
'label' => 'Enrollment Limits',
|
||||||
|
'category' => 'Enrollment',
|
||||||
|
'platform' => 'all',
|
||||||
|
'endpoint' => 'deviceManagement/deviceEnrollmentConfigurations',
|
||||||
|
'backup' => 'full',
|
||||||
|
'restore' => 'preview-only',
|
||||||
|
'risk' => 'high',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'type' => 'deviceEnrollmentPlatformRestrictionsConfiguration',
|
||||||
|
'label' => 'Platform Restrictions (Enrollment)',
|
||||||
|
'category' => 'Enrollment',
|
||||||
|
'platform' => 'all',
|
||||||
|
'endpoint' => 'deviceManagement/deviceEnrollmentConfigurations',
|
||||||
|
'backup' => 'full',
|
||||||
|
'restore' => 'preview-only',
|
||||||
|
'risk' => 'high',
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'type' => 'deviceEnrollmentNotificationConfiguration',
|
||||||
|
'label' => 'Enrollment Notifications',
|
||||||
|
'category' => 'Enrollment',
|
||||||
|
'platform' => 'all',
|
||||||
|
'endpoint' => 'deviceManagement/deviceEnrollmentConfigurations',
|
||||||
|
'filter' => "deviceEnrollmentConfigurationType eq 'EnrollmentNotificationsConfiguration'",
|
||||||
|
'backup' => 'full',
|
||||||
|
'restore' => 'preview-only',
|
||||||
|
'risk' => 'high',
|
||||||
|
],
|
||||||
[
|
[
|
||||||
'type' => 'enrollmentRestriction',
|
'type' => 'enrollmentRestriction',
|
||||||
'label' => 'Enrollment Restrictions',
|
'label' => 'Enrollment Restrictions',
|
||||||
|
|||||||
@ -8,21 +8,21 @@ ## Phase 1: Setup
|
|||||||
- [x] T001 Create spec/plan/tasks and checklist.
|
- [x] T001 Create spec/plan/tasks and checklist.
|
||||||
|
|
||||||
## Phase 2: Research & Design
|
## Phase 2: Research & Design
|
||||||
- [ ] T002 Confirm `@odata.type` for each subtype and whether Graph supports assignments.
|
- [x] T002 Confirm `@odata.type` for each subtype and whether Graph supports assignments.
|
||||||
- [ ] T003 Decide restore modes and risk levels.
|
- [x] T003 Decide restore modes and risk levels.
|
||||||
|
|
||||||
## Phase 3: Tests (TDD)
|
## Phase 3: Tests (TDD)
|
||||||
- [ ] T004 Add sync tests ensuring each subtype is classified correctly.
|
- [x] T004 Add sync tests ensuring each subtype is classified correctly.
|
||||||
- [ ] T005 Add snapshot capture test for at least one subtype.
|
- [x] T005 Add snapshot capture test for at least one subtype.
|
||||||
- [ ] T006 Add restore preview test ensuring preview-only behavior.
|
- [x] T006 Add restore preview test ensuring preview-only behavior.
|
||||||
|
|
||||||
## Phase 4: Implementation
|
## Phase 4: Implementation
|
||||||
- [ ] T007 Add new types to `config/tenantpilot.php`.
|
- [x] T007 Add new types to `config/tenantpilot.php`.
|
||||||
- [ ] T008 Add contracts in `config/graph_contracts.php` (resource + type families).
|
- [x] T008 Add contracts in `config/graph_contracts.php` (resource + type families).
|
||||||
- [ ] T009 Update `PolicySyncService` enrollment classification logic.
|
- [x] T009 Update `PolicySyncService` enrollment classification logic.
|
||||||
- [ ] T010 Add normalizer for readable UI output (key fields per subtype).
|
- [x] T010 Add normalizer for readable UI output (key fields per subtype).
|
||||||
|
- [x] T013 Hydrate notification templates for enrollment notifications.
|
||||||
|
|
||||||
## Phase 5: Verification
|
## Phase 5: Verification
|
||||||
- [ ] T011 Run targeted tests.
|
- [x] T011 Run targeted tests.
|
||||||
- [ ] T012 Run Pint (`./vendor/bin/pint --dirty`).
|
- [x] T012 Run Pint (`./vendor/bin/pint --dirty`).
|
||||||
|
|
||||||
|
|||||||
@ -109,11 +109,11 @@
|
|||||||
$response->assertSee('app-2');
|
$response->assertSee('app-2');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('policy detail renders normalized settings for enrollment restrictions', function () {
|
test('policy detail renders normalized settings for platform restrictions (enrollment)', function () {
|
||||||
$policy = Policy::create([
|
$policy = Policy::create([
|
||||||
'tenant_id' => $this->tenant->id,
|
'tenant_id' => $this->tenant->id,
|
||||||
'external_id' => 'enroll-restrict-1',
|
'external_id' => 'enroll-restrict-1',
|
||||||
'policy_type' => 'enrollmentRestriction',
|
'policy_type' => 'deviceEnrollmentPlatformRestrictionsConfiguration',
|
||||||
'display_name' => 'Restriction A',
|
'display_name' => 'Restriction A',
|
||||||
'platform' => 'all',
|
'platform' => 'all',
|
||||||
]);
|
]);
|
||||||
@ -143,9 +143,9 @@
|
|||||||
|
|
||||||
$response->assertOk();
|
$response->assertOk();
|
||||||
$response->assertSee('Settings');
|
$response->assertSee('Settings');
|
||||||
$response->assertSee('Enrollment restrictions');
|
$response->assertSee('Platform restrictions (enrollment)');
|
||||||
$response->assertSee('Personal device enrollment blocked');
|
$response->assertSee('Platform: Personal device enrollment blocked');
|
||||||
$response->assertSee('Enabled');
|
$response->assertSee('Enabled');
|
||||||
$response->assertSee('Blocked SKUs');
|
$response->assertSee('Platform: Blocked SKUs');
|
||||||
$response->assertSee('sku-1');
|
$response->assertSee('sku-1');
|
||||||
});
|
});
|
||||||
|
|||||||
@ -109,3 +109,103 @@ public function request(string $method, string $path, array $options = []): Grap
|
|||||||
|
|
||||||
expect($client->applyCalls)->toBe(0);
|
expect($client->applyCalls)->toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('enrollment limit restores are preview-only and skipped on execution', function () {
|
||||||
|
$client = new class implements GraphClientInterface
|
||||||
|
{
|
||||||
|
public int $applyCalls = 0;
|
||||||
|
|
||||||
|
public function listPolicies(string $policyType, array $options = []): GraphResponse
|
||||||
|
{
|
||||||
|
return new GraphResponse(true, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPolicy(string $policyType, string $policyId, array $options = []): GraphResponse
|
||||||
|
{
|
||||||
|
return new GraphResponse(true, ['payload' => []]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getOrganization(array $options = []): GraphResponse
|
||||||
|
{
|
||||||
|
return new GraphResponse(true, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function applyPolicy(string $policyType, string $policyId, array $payload, array $options = []): GraphResponse
|
||||||
|
{
|
||||||
|
$this->applyCalls++;
|
||||||
|
|
||||||
|
return new GraphResponse(true, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getServicePrincipalPermissions(array $options = []): GraphResponse
|
||||||
|
{
|
||||||
|
return new GraphResponse(true, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function request(string $method, string $path, array $options = []): GraphResponse
|
||||||
|
{
|
||||||
|
return new GraphResponse(true, []);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
app()->instance(GraphClientInterface::class, $client);
|
||||||
|
|
||||||
|
$tenant = Tenant::create([
|
||||||
|
'tenant_id' => 'tenant-enrollment-limit',
|
||||||
|
'name' => 'Tenant Enrollment Limit',
|
||||||
|
'metadata' => [],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$policy = Policy::create([
|
||||||
|
'tenant_id' => $tenant->id,
|
||||||
|
'external_id' => 'enrollment-limit-1',
|
||||||
|
'policy_type' => 'deviceEnrollmentLimitConfiguration',
|
||||||
|
'display_name' => 'Enrollment Limit',
|
||||||
|
'platform' => 'all',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$backupSet = BackupSet::create([
|
||||||
|
'tenant_id' => $tenant->id,
|
||||||
|
'name' => 'Enrollment Limit Backup',
|
||||||
|
'status' => 'completed',
|
||||||
|
'item_count' => 1,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$backupItem = BackupItem::create([
|
||||||
|
'tenant_id' => $tenant->id,
|
||||||
|
'backup_set_id' => $backupSet->id,
|
||||||
|
'policy_id' => $policy->id,
|
||||||
|
'policy_identifier' => $policy->external_id,
|
||||||
|
'policy_type' => $policy->policy_type,
|
||||||
|
'platform' => $policy->platform,
|
||||||
|
'payload' => [
|
||||||
|
'@odata.type' => '#microsoft.graph.deviceEnrollmentLimitConfiguration',
|
||||||
|
'id' => $policy->external_id,
|
||||||
|
'displayName' => $policy->display_name,
|
||||||
|
'limit' => 5,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$service = app(RestoreService::class);
|
||||||
|
$preview = $service->preview($tenant, $backupSet, [$backupItem->id]);
|
||||||
|
|
||||||
|
$previewItem = collect($preview)->first(fn (array $item) => ($item['policy_type'] ?? null) === 'deviceEnrollmentLimitConfiguration');
|
||||||
|
|
||||||
|
expect($previewItem)->not->toBeNull()
|
||||||
|
->and($previewItem['restore_mode'] ?? null)->toBe('preview-only');
|
||||||
|
|
||||||
|
$run = $service->execute(
|
||||||
|
tenant: $tenant,
|
||||||
|
backupSet: $backupSet,
|
||||||
|
selectedItemIds: [$backupItem->id],
|
||||||
|
dryRun: false,
|
||||||
|
actorEmail: 'tester@example.com',
|
||||||
|
actorName: 'Tester',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect($run->results)->toHaveCount(1);
|
||||||
|
expect($run->results[0]['status'])->toBe('skipped');
|
||||||
|
expect($run->results[0]['reason'])->toBe('preview_only');
|
||||||
|
|
||||||
|
expect($client->applyCalls)->toBe(0);
|
||||||
|
});
|
||||||
|
|||||||
@ -56,3 +56,95 @@
|
|||||||
$response->assertSee('Enable feature');
|
$response->assertSee('Enable feature');
|
||||||
$response->assertSee('Normalized diff');
|
$response->assertSee('Normalized diff');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('policy version detail shows enrollment notification template settings', function () {
|
||||||
|
$tenant = Tenant::create([
|
||||||
|
'tenant_id' => 'tenant-enrollment-notify',
|
||||||
|
'name' => 'Tenant Enrollment Notify',
|
||||||
|
'metadata' => [],
|
||||||
|
'is_current' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$tenant->makeCurrent();
|
||||||
|
|
||||||
|
$policy = Policy::create([
|
||||||
|
'tenant_id' => $tenant->id,
|
||||||
|
'external_id' => 'enroll-notify-1',
|
||||||
|
'policy_type' => 'deviceEnrollmentNotificationConfiguration',
|
||||||
|
'display_name' => 'Enrollment Notifications',
|
||||||
|
'platform' => 'all',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$version = PolicyVersion::create([
|
||||||
|
'tenant_id' => $tenant->id,
|
||||||
|
'policy_id' => $policy->id,
|
||||||
|
'version_number' => 1,
|
||||||
|
'policy_type' => $policy->policy_type,
|
||||||
|
'platform' => $policy->platform,
|
||||||
|
'created_by' => 'tester@example.com',
|
||||||
|
'captured_at' => CarbonImmutable::now(),
|
||||||
|
'snapshot' => [
|
||||||
|
'@odata.type' => '#microsoft.graph.deviceEnrollmentNotificationConfiguration',
|
||||||
|
'displayName' => 'Enrollment Notifications',
|
||||||
|
'priority' => 1,
|
||||||
|
'version' => 1,
|
||||||
|
'platformType' => 'windows',
|
||||||
|
'notificationTemplates' => ['Email_email-template-1', 'Push_push-template-1'],
|
||||||
|
'notificationTemplateSnapshots' => [
|
||||||
|
[
|
||||||
|
'channel' => 'Email',
|
||||||
|
'template_id' => 'email-template-1',
|
||||||
|
'template' => [
|
||||||
|
'id' => 'email-template-1',
|
||||||
|
'displayName' => 'Email Template',
|
||||||
|
'defaultLocale' => 'en-us',
|
||||||
|
'brandingOptions' => 'none',
|
||||||
|
],
|
||||||
|
'localized_notification_messages' => [
|
||||||
|
[
|
||||||
|
'locale' => 'en-us',
|
||||||
|
'subject' => 'Email Subject',
|
||||||
|
'messageTemplate' => 'Email Body',
|
||||||
|
'isDefault' => true,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'channel' => 'Push',
|
||||||
|
'template_id' => 'push-template-1',
|
||||||
|
'template' => [
|
||||||
|
'id' => 'push-template-1',
|
||||||
|
'displayName' => 'Push Template',
|
||||||
|
'defaultLocale' => 'en-us',
|
||||||
|
'brandingOptions' => 'none',
|
||||||
|
],
|
||||||
|
'localized_notification_messages' => [
|
||||||
|
[
|
||||||
|
'locale' => 'en-us',
|
||||||
|
'subject' => 'Push Subject',
|
||||||
|
'messageTemplate' => 'Push Body',
|
||||||
|
'isDefault' => true,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$user = User::factory()->create();
|
||||||
|
|
||||||
|
$response = $this->actingAs($user)
|
||||||
|
->get(PolicyVersionResource::getUrl('view', ['record' => $version]).'?tab=normalized-settings');
|
||||||
|
|
||||||
|
$response->assertOk();
|
||||||
|
$response->assertSee('Enrollment notifications');
|
||||||
|
$response->assertSee('Notification templates');
|
||||||
|
$response->assertSee('Email (en-us) Subject');
|
||||||
|
$response->assertSee('Email Subject');
|
||||||
|
$response->assertSee('Email (en-us) Message');
|
||||||
|
$response->assertSee('Email Body');
|
||||||
|
$response->assertSee('Push (en-us) Subject');
|
||||||
|
$response->assertSee('Push Subject');
|
||||||
|
$response->assertSee('Push (en-us) Message');
|
||||||
|
$response->assertSee('Push Body');
|
||||||
|
});
|
||||||
|
|||||||
@ -172,7 +172,39 @@ public function request(string $method, string $path, array $options = []): Grap
|
|||||||
$response = $this->get(route('filament.admin.resources.tenants.view', $tenant));
|
$response = $this->get(route('filament.admin.resources.tenants.view', $tenant));
|
||||||
|
|
||||||
$response->assertOk();
|
$response->assertOk();
|
||||||
|
$response->assertSee('Actions');
|
||||||
$response->assertSee($firstKey);
|
$response->assertSee($firstKey);
|
||||||
$response->assertSee('ok');
|
$response->assertSee('ok');
|
||||||
$response->assertSee('missing');
|
$response->assertSee('missing');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('tenant list shows Open in Entra action', function () {
|
||||||
|
$user = User::factory()->create();
|
||||||
|
$this->actingAs($user);
|
||||||
|
|
||||||
|
Tenant::create([
|
||||||
|
'tenant_id' => 'tenant-ui-list',
|
||||||
|
'name' => 'UI Tenant List',
|
||||||
|
'app_client_id' => 'client-123',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $this->get(route('filament.admin.resources.tenants.index'));
|
||||||
|
|
||||||
|
$response->assertOk();
|
||||||
|
$response->assertSee('Open in Entra');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('tenant can be deactivated from the tenant detail action menu', function () {
|
||||||
|
$user = User::factory()->create();
|
||||||
|
$this->actingAs($user);
|
||||||
|
|
||||||
|
$tenant = Tenant::create([
|
||||||
|
'tenant_id' => 'tenant-ui-deactivate',
|
||||||
|
'name' => 'UI Tenant Deactivate',
|
||||||
|
]);
|
||||||
|
|
||||||
|
Livewire::test(ViewTenant::class, ['record' => $tenant->getRouteKey()])
|
||||||
|
->callAction('archive');
|
||||||
|
|
||||||
|
$this->assertSoftDeleted('tenants', ['id' => $tenant->id]);
|
||||||
|
});
|
||||||
|
|||||||
@ -106,7 +106,11 @@
|
|||||||
|
|
||||||
$mock->shouldReceive('listPolicies')
|
$mock->shouldReceive('listPolicies')
|
||||||
->andReturnUsing(function (string $policyType) use ($payload) {
|
->andReturnUsing(function (string $policyType) use ($payload) {
|
||||||
if (in_array($policyType, ['enrollmentRestriction', 'windowsEnrollmentStatusPage'], true)) {
|
if (in_array($policyType, [
|
||||||
|
'enrollmentRestriction',
|
||||||
|
'windowsEnrollmentStatusPage',
|
||||||
|
'deviceEnrollmentPlatformRestrictionsConfiguration',
|
||||||
|
], true)) {
|
||||||
return new GraphResponse(true, $payload);
|
return new GraphResponse(true, $payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -122,6 +126,11 @@
|
|||||||
'platform' => 'all',
|
'platform' => 'all',
|
||||||
'filter' => null,
|
'filter' => null,
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
'type' => 'deviceEnrollmentPlatformRestrictionsConfiguration',
|
||||||
|
'platform' => 'all',
|
||||||
|
'filter' => null,
|
||||||
|
],
|
||||||
[
|
[
|
||||||
'type' => 'enrollmentRestriction',
|
'type' => 'enrollmentRestriction',
|
||||||
'platform' => 'all',
|
'platform' => 'all',
|
||||||
@ -142,6 +151,100 @@
|
|||||||
->pluck('external_id')
|
->pluck('external_id')
|
||||||
->all();
|
->all();
|
||||||
|
|
||||||
|
$platformRestrictionIds = Policy::query()
|
||||||
|
->where('tenant_id', $tenant->id)
|
||||||
|
->where('policy_type', 'deviceEnrollmentPlatformRestrictionsConfiguration')
|
||||||
|
->orderBy('external_id')
|
||||||
|
->pluck('external_id')
|
||||||
|
->all();
|
||||||
|
|
||||||
expect($espIds)->toMatchArray(['esp-1']);
|
expect($espIds)->toMatchArray(['esp-1']);
|
||||||
expect($restrictionIds)->toMatchArray(['other-1', 'restriction-1']);
|
expect($platformRestrictionIds)->toMatchArray(['restriction-1']);
|
||||||
|
expect($restrictionIds)->toMatchArray(['other-1']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('policy sync classifies enrollment configuration subtypes separately', function () {
|
||||||
|
$tenant = Tenant::create([
|
||||||
|
'tenant_id' => 'tenant-sync-enrollment-subtypes',
|
||||||
|
'name' => 'Tenant Sync Enrollment Subtypes',
|
||||||
|
'metadata' => [],
|
||||||
|
'is_current' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$tenant->makeCurrent();
|
||||||
|
|
||||||
|
$this->mock(GraphClientInterface::class, function (MockInterface $mock) {
|
||||||
|
$limitPayload = [
|
||||||
|
'id' => 'limit-1',
|
||||||
|
'displayName' => 'Enrollment Limit',
|
||||||
|
'@odata.type' => '#microsoft.graph.deviceEnrollmentLimitConfiguration',
|
||||||
|
'deviceEnrollmentConfigurationType' => 'deviceEnrollmentLimitConfiguration',
|
||||||
|
'limit' => 5,
|
||||||
|
];
|
||||||
|
|
||||||
|
$platformRestrictionsPayload = [
|
||||||
|
'id' => 'platform-1',
|
||||||
|
'displayName' => 'Platform Restrictions',
|
||||||
|
'@odata.type' => '#microsoft.graph.deviceEnrollmentPlatformRestrictionsConfiguration',
|
||||||
|
'deviceEnrollmentConfigurationType' => 'deviceEnrollmentPlatformRestrictionsConfiguration',
|
||||||
|
];
|
||||||
|
|
||||||
|
$notificationPayload = [
|
||||||
|
'id' => 'notify-1',
|
||||||
|
'displayName' => 'Enrollment Notifications',
|
||||||
|
'@odata.type' => '#microsoft.graph.deviceEnrollmentNotificationConfiguration',
|
||||||
|
'deviceEnrollmentConfigurationType' => 'EnrollmentNotificationsConfiguration',
|
||||||
|
];
|
||||||
|
|
||||||
|
$unfilteredPayload = [
|
||||||
|
$limitPayload,
|
||||||
|
$platformRestrictionsPayload,
|
||||||
|
$notificationPayload,
|
||||||
|
];
|
||||||
|
|
||||||
|
$mock->shouldReceive('listPolicies')
|
||||||
|
->andReturnUsing(function (string $policyType) use ($notificationPayload, $unfilteredPayload) {
|
||||||
|
if ($policyType === 'deviceEnrollmentNotificationConfiguration') {
|
||||||
|
return new GraphResponse(true, [$notificationPayload]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (in_array($policyType, [
|
||||||
|
'enrollmentRestriction',
|
||||||
|
'deviceEnrollmentLimitConfiguration',
|
||||||
|
'deviceEnrollmentPlatformRestrictionsConfiguration',
|
||||||
|
'windowsEnrollmentStatusPage',
|
||||||
|
], true)) {
|
||||||
|
return new GraphResponse(true, $unfilteredPayload);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new GraphResponse(true, []);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
$service = app(PolicySyncService::class);
|
||||||
|
|
||||||
|
$service->syncPolicies($tenant, [
|
||||||
|
['type' => 'deviceEnrollmentLimitConfiguration', 'platform' => 'all', 'filter' => null],
|
||||||
|
['type' => 'deviceEnrollmentPlatformRestrictionsConfiguration', 'platform' => 'all', 'filter' => null],
|
||||||
|
['type' => 'deviceEnrollmentNotificationConfiguration', 'platform' => 'all', 'filter' => null],
|
||||||
|
['type' => 'enrollmentRestriction', 'platform' => 'all', 'filter' => null],
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(Policy::query()
|
||||||
|
->where('tenant_id', $tenant->id)
|
||||||
|
->where('policy_type', 'deviceEnrollmentLimitConfiguration')
|
||||||
|
->pluck('external_id')
|
||||||
|
->all())->toMatchArray(['limit-1']);
|
||||||
|
|
||||||
|
expect(Policy::query()
|
||||||
|
->where('tenant_id', $tenant->id)
|
||||||
|
->where('policy_type', 'deviceEnrollmentPlatformRestrictionsConfiguration')
|
||||||
|
->pluck('external_id')
|
||||||
|
->all())->toMatchArray(['platform-1']);
|
||||||
|
|
||||||
|
expect(Policy::query()
|
||||||
|
->where('tenant_id', $tenant->id)
|
||||||
|
->where('policy_type', 'deviceEnrollmentNotificationConfiguration')
|
||||||
|
->pluck('external_id')
|
||||||
|
->all())->toMatchArray(['notify-1']);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -98,6 +98,76 @@
|
|||||||
expect($version->assignments[0]['target']['assignment_filter_name'])->toBe('Targeted Devices');
|
expect($version->assignments[0]['target']['assignment_filter_name'])->toBe('Targeted Devices');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('captures enrollment limit configuration version with assignments from graph', function () {
|
||||||
|
$this->policy->forceFill([
|
||||||
|
'policy_type' => 'deviceEnrollmentLimitConfiguration',
|
||||||
|
'platform' => 'all',
|
||||||
|
])->save();
|
||||||
|
|
||||||
|
$this->mock(PolicySnapshotService::class, function ($mock) {
|
||||||
|
$mock->shouldReceive('fetch')
|
||||||
|
->once()
|
||||||
|
->andReturn([
|
||||||
|
'payload' => [
|
||||||
|
'@odata.type' => '#microsoft.graph.deviceEnrollmentLimitConfiguration',
|
||||||
|
'id' => 'test-policy-id',
|
||||||
|
'displayName' => 'Enrollment Limit',
|
||||||
|
'limit' => 5,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->mock(AssignmentFetcher::class, function ($mock) {
|
||||||
|
$mock->shouldReceive('fetch')
|
||||||
|
->once()
|
||||||
|
->withArgs(function (string $policyType): bool {
|
||||||
|
return $policyType === 'deviceEnrollmentLimitConfiguration';
|
||||||
|
})
|
||||||
|
->andReturn([
|
||||||
|
[
|
||||||
|
'id' => 'assignment-1',
|
||||||
|
'intent' => 'apply',
|
||||||
|
'target' => [
|
||||||
|
'@odata.type' => '#microsoft.graph.groupAssignmentTarget',
|
||||||
|
'groupId' => 'group-123',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->mock(GroupResolver::class, function ($mock) {
|
||||||
|
$mock->shouldReceive('resolveGroupIds')
|
||||||
|
->once()
|
||||||
|
->andReturn([
|
||||||
|
'group-123' => [
|
||||||
|
'id' => 'group-123',
|
||||||
|
'displayName' => 'Test Group',
|
||||||
|
'orphaned' => false,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->mock(AssignmentFilterResolver::class, function ($mock) {
|
||||||
|
$mock->shouldReceive('resolve')
|
||||||
|
->once()
|
||||||
|
->andReturn([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
$versionService = app(VersionService::class);
|
||||||
|
$version = $versionService->captureFromGraph(
|
||||||
|
$this->tenant,
|
||||||
|
$this->policy,
|
||||||
|
'test@example.com'
|
||||||
|
);
|
||||||
|
|
||||||
|
expect($version)->not->toBeNull()
|
||||||
|
->and($version->policy_type)->toBe('deviceEnrollmentLimitConfiguration')
|
||||||
|
->and($version->snapshot['@odata.type'] ?? null)->toBe('#microsoft.graph.deviceEnrollmentLimitConfiguration')
|
||||||
|
->and($version->snapshot['limit'] ?? null)->toBe(5)
|
||||||
|
->and($version->assignments)->toHaveCount(1)
|
||||||
|
->and($version->metadata['assignments_count'])->toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
it('hydrates assignment filter names when filter data is stored at root', function () {
|
it('hydrates assignment filter names when filter data is stored at root', function () {
|
||||||
$this->mock(PolicySnapshotService::class, function ($mock) {
|
$this->mock(PolicySnapshotService::class, function ($mock) {
|
||||||
$mock->shouldReceive('fetch')
|
$mock->shouldReceive('fetch')
|
||||||
|
|||||||
@ -67,34 +67,30 @@
|
|||||||
expect(collect($result['warnings'])->join(' '))->toContain('@odata.type mismatch');
|
expect(collect($result['warnings'])->join(' '))->toContain('@odata.type mismatch');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('normalizes enrollment restrictions platform restriction payload', function () {
|
it('normalizes enrollment platform restriction payload', function () {
|
||||||
$snapshot = [
|
$snapshot = [
|
||||||
'@odata.type' => '#microsoft.graph.deviceEnrollmentPlatformRestrictionConfiguration',
|
'@odata.type' => '#microsoft.graph.deviceEnrollmentPlatformRestrictionConfiguration',
|
||||||
'deviceEnrollmentConfigurationType' => 'deviceEnrollmentPlatformRestrictionConfiguration',
|
'deviceEnrollmentConfigurationType' => 'deviceEnrollmentPlatformRestrictionConfiguration',
|
||||||
'displayName' => 'DeviceTypeRestriction',
|
'displayName' => 'DeviceTypeRestriction',
|
||||||
'version' => 2,
|
'version' => 2,
|
||||||
// Graph uses this singular shape for platform restriction configs.
|
|
||||||
'platformRestriction' => [
|
'platformRestriction' => [
|
||||||
'platformBlocked' => false,
|
'platformBlocked' => false,
|
||||||
'personalDeviceEnrollmentBlocked' => true,
|
'personalDeviceEnrollmentBlocked' => true,
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
$result = $this->normalizer->normalize($snapshot, 'enrollmentRestriction', 'all');
|
$result = $this->normalizer->normalize($snapshot, 'deviceEnrollmentPlatformRestrictionsConfiguration', 'all');
|
||||||
|
|
||||||
$block = collect($result['settings'])->firstWhere('title', 'Enrollment restrictions');
|
$block = collect($result['settings'])->firstWhere('title', 'Platform restrictions (enrollment)');
|
||||||
expect($block)->not->toBeNull();
|
expect($block)->not->toBeNull();
|
||||||
|
|
||||||
$platformEntry = collect($block['entries'] ?? [])->firstWhere('key', 'Platform restrictions');
|
expect(collect($block['entries'] ?? [])->firstWhere('key', 'Platform: Platform blocked')['value'] ?? null)->toBe('Disabled');
|
||||||
expect($platformEntry)->toBeNull();
|
expect(collect($block['entries'] ?? [])->firstWhere('key', 'Platform: Personal device enrollment blocked')['value'] ?? null)->toBe('Enabled');
|
||||||
|
|
||||||
expect(collect($block['entries'] ?? [])->firstWhere('key', 'Platform blocked')['value'] ?? null)->toBe('Disabled');
|
expect(collect($block['entries'] ?? [])->firstWhere('key', 'Platform: OS minimum version')['value'] ?? null)->toBe('None');
|
||||||
expect(collect($block['entries'] ?? [])->firstWhere('key', 'Personal device enrollment blocked')['value'] ?? null)->toBe('Enabled');
|
expect(collect($block['entries'] ?? [])->firstWhere('key', 'Platform: OS maximum version')['value'] ?? null)->toBe('None');
|
||||||
|
expect(collect($block['entries'] ?? [])->firstWhere('key', 'Platform: Blocked manufacturers')['value'] ?? null)->toBe(['None']);
|
||||||
expect(collect($block['entries'] ?? [])->firstWhere('key', 'OS minimum version')['value'] ?? null)->toBe('None');
|
expect(collect($block['entries'] ?? [])->firstWhere('key', 'Platform: Blocked SKUs')['value'] ?? null)->toBe(['None']);
|
||||||
expect(collect($block['entries'] ?? [])->firstWhere('key', 'OS maximum version')['value'] ?? null)->toBe('None');
|
|
||||||
expect(collect($block['entries'] ?? [])->firstWhere('key', 'Blocked manufacturers')['value'] ?? null)->toBe(['None']);
|
|
||||||
expect(collect($block['entries'] ?? [])->firstWhere('key', 'Blocked SKUs')['value'] ?? null)->toBe(['None']);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('normalizes Autopilot deployment profile key fields', function () {
|
it('normalizes Autopilot deployment profile key fields', function () {
|
||||||
|
|||||||
@ -172,6 +172,120 @@ public function request(string $method, string $path, array $options = []): Grap
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class EnrollmentNotificationSnapshotGraphClient implements GraphClientInterface
|
||||||
|
{
|
||||||
|
public array $requests = [];
|
||||||
|
|
||||||
|
public function listPolicies(string $policyType, array $options = []): GraphResponse
|
||||||
|
{
|
||||||
|
return new GraphResponse(success: true, data: []);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPolicy(string $policyType, string $policyId, array $options = []): GraphResponse
|
||||||
|
{
|
||||||
|
$this->requests[] = ['getPolicy', $policyType, $policyId, $options];
|
||||||
|
|
||||||
|
if ($policyType === 'deviceEnrollmentNotificationConfiguration') {
|
||||||
|
return new GraphResponse(success: true, data: [
|
||||||
|
'payload' => [
|
||||||
|
'id' => $policyId,
|
||||||
|
'displayName' => 'Enrollment Notifications',
|
||||||
|
'@odata.type' => '#microsoft.graph.deviceEnrollmentNotificationConfiguration',
|
||||||
|
'priority' => 1,
|
||||||
|
'version' => 1,
|
||||||
|
'platformType' => 'windows',
|
||||||
|
'brandingOptions' => 'none',
|
||||||
|
'templateType' => '0',
|
||||||
|
'notificationMessageTemplateId' => '00000000-0000-0000-0000-000000000000',
|
||||||
|
'notificationTemplates' => [
|
||||||
|
'Email_email-template-1',
|
||||||
|
'Push_push-template-1',
|
||||||
|
],
|
||||||
|
'deviceEnrollmentConfigurationType' => 'enrollmentNotificationsConfiguration',
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new GraphResponse(success: true, data: [
|
||||||
|
'payload' => [
|
||||||
|
'id' => $policyId,
|
||||||
|
'displayName' => 'Policy',
|
||||||
|
'@odata.type' => '#microsoft.graph.deviceConfiguration',
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getOrganization(array $options = []): GraphResponse
|
||||||
|
{
|
||||||
|
return new GraphResponse(success: true, data: []);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function applyPolicy(string $policyType, string $policyId, array $payload, array $options = []): GraphResponse
|
||||||
|
{
|
||||||
|
return new GraphResponse(success: true, data: []);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getServicePrincipalPermissions(array $options = []): GraphResponse
|
||||||
|
{
|
||||||
|
return new GraphResponse(success: true, data: []);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function request(string $method, string $path, array $options = []): GraphResponse
|
||||||
|
{
|
||||||
|
$this->requests[] = [$method, $path, $options];
|
||||||
|
|
||||||
|
if ($method === 'GET' && str_contains($path, 'deviceManagement/notificationMessageTemplates/email-template-1/localizedNotificationMessages')) {
|
||||||
|
return new GraphResponse(success: true, data: [
|
||||||
|
'value' => [
|
||||||
|
[
|
||||||
|
'id' => 'email-template-1_en-us',
|
||||||
|
'locale' => 'en-us',
|
||||||
|
'subject' => 'Email Subject',
|
||||||
|
'messageTemplate' => 'Email Body',
|
||||||
|
'isDefault' => true,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($method === 'GET' && str_contains($path, 'deviceManagement/notificationMessageTemplates/push-template-1/localizedNotificationMessages')) {
|
||||||
|
return new GraphResponse(success: true, data: [
|
||||||
|
'value' => [
|
||||||
|
[
|
||||||
|
'id' => 'push-template-1_en-us',
|
||||||
|
'locale' => 'en-us',
|
||||||
|
'subject' => 'Push Subject',
|
||||||
|
'messageTemplate' => 'Push Body',
|
||||||
|
'isDefault' => true,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($method === 'GET' && str_contains($path, 'deviceManagement/notificationMessageTemplates/email-template-1')) {
|
||||||
|
return new GraphResponse(success: true, data: [
|
||||||
|
'@odata.context' => 'https://graph.microsoft.com/beta/$metadata#deviceManagement/notificationMessageTemplates/$entity',
|
||||||
|
'id' => 'email-template-1',
|
||||||
|
'displayName' => 'Email Template',
|
||||||
|
'defaultLocale' => 'en-us',
|
||||||
|
'brandingOptions' => 'none',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($method === 'GET' && str_contains($path, 'deviceManagement/notificationMessageTemplates/push-template-1')) {
|
||||||
|
return new GraphResponse(success: true, data: [
|
||||||
|
'@odata.context' => 'https://graph.microsoft.com/beta/$metadata#deviceManagement/notificationMessageTemplates/$entity',
|
||||||
|
'id' => 'push-template-1',
|
||||||
|
'displayName' => 'Push Template',
|
||||||
|
'defaultLocale' => 'en-us',
|
||||||
|
'brandingOptions' => 'none',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new GraphResponse(success: true, data: []);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
it('hydrates compliance policy scheduled actions into snapshots', function () {
|
it('hydrates compliance policy scheduled actions into snapshots', function () {
|
||||||
$client = new PolicySnapshotGraphClient;
|
$client = new PolicySnapshotGraphClient;
|
||||||
app()->instance(GraphClientInterface::class, $client);
|
app()->instance(GraphClientInterface::class, $client);
|
||||||
@ -247,6 +361,47 @@ public function request(string $method, string $path, array $options = []): Grap
|
|||||||
'securityBaselinePolicy',
|
'securityBaselinePolicy',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
it('hydrates enrollment notification templates into snapshots', function () {
|
||||||
|
$client = new EnrollmentNotificationSnapshotGraphClient;
|
||||||
|
app()->instance(GraphClientInterface::class, $client);
|
||||||
|
|
||||||
|
$tenant = Tenant::factory()->create([
|
||||||
|
'tenant_id' => 'tenant-enrollment-notifications',
|
||||||
|
'app_client_id' => 'client-123',
|
||||||
|
'app_client_secret' => 'secret-123',
|
||||||
|
'is_current' => true,
|
||||||
|
]);
|
||||||
|
$tenant->makeCurrent();
|
||||||
|
|
||||||
|
$policy = Policy::factory()->create([
|
||||||
|
'tenant_id' => $tenant->id,
|
||||||
|
'external_id' => 'enroll-notify-123',
|
||||||
|
'policy_type' => 'deviceEnrollmentNotificationConfiguration',
|
||||||
|
'display_name' => 'Enrollment Notifications',
|
||||||
|
'platform' => 'all',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$service = app(PolicySnapshotService::class);
|
||||||
|
$result = $service->fetch($tenant, $policy);
|
||||||
|
|
||||||
|
expect($result)->toHaveKey('payload');
|
||||||
|
expect($result['payload'])->toHaveKey('notificationTemplateSnapshots');
|
||||||
|
expect($result['payload']['notificationTemplateSnapshots'])->toHaveCount(2);
|
||||||
|
expect($result['metadata']['enrollment_notification_templates_hydration'] ?? null)->toBe('complete');
|
||||||
|
|
||||||
|
$email = collect($result['payload']['notificationTemplateSnapshots'])->firstWhere('channel', 'Email');
|
||||||
|
expect($email)->not->toBeNull()
|
||||||
|
->and($email['template_id'] ?? null)->toBe('email-template-1')
|
||||||
|
->and($email['template']['displayName'] ?? null)->toBe('Email Template')
|
||||||
|
->and($email['localized_notification_messages'][0]['subject'] ?? null)->toBe('Email Subject');
|
||||||
|
|
||||||
|
$push = collect($result['payload']['notificationTemplateSnapshots'])->firstWhere('channel', 'Push');
|
||||||
|
expect($push)->not->toBeNull()
|
||||||
|
->and($push['template_id'] ?? null)->toBe('push-template-1')
|
||||||
|
->and($push['template']['displayName'] ?? null)->toBe('Push Template')
|
||||||
|
->and($push['localized_notification_messages'][0]['subject'] ?? null)->toBe('Push Subject');
|
||||||
|
});
|
||||||
|
|
||||||
it('filters mobile app snapshots to metadata-only keys', function () {
|
it('filters mobile app snapshots to metadata-only keys', function () {
|
||||||
$client = new PolicySnapshotGraphClient;
|
$client = new PolicySnapshotGraphClient;
|
||||||
app()->instance(GraphClientInterface::class, $client);
|
app()->instance(GraphClientInterface::class, $client);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user