feat: mobileApp metadata-only + assignment restore

This commit is contained in:
Ahmed Darrazi 2025-12-29 14:03:13 +01:00
parent a477ebce49
commit 623d6904a0
4 changed files with 172 additions and 0 deletions

View File

@ -46,6 +46,14 @@ public function fetch(Tenant $tenant, Policy $policy, ?string $actorEmail = null
'platform' => $policy->platform,
];
if ($this->isMetadataOnlyPolicyType($policy->policy_type)) {
$select = $this->metadataOnlySelect($policy->policy_type);
if ($select !== []) {
$options['select'] = $select;
}
}
if ($policy->policy_type === 'deviceCompliancePolicy') {
$options['expand'] = 'scheduledActionsForRule($expand=scheduledActionConfigurations)';
}
@ -110,6 +118,10 @@ public function fetch(Tenant $tenant, Policy $policy, ?string $actorEmail = null
$metadataWarnings = $response->warnings ?? [$reason];
}
if (! $response->failed() && $this->isMetadataOnlyPolicyType($policy->policy_type)) {
$payload = $this->filterMetadataOnlyPayload($policy->policy_type, is_array($payload) ? $payload : []);
}
$validation = $this->snapshotValidator->validate(is_array($payload) ? $payload : []);
$metadataWarnings = array_merge($metadataWarnings, $validation['warnings']);
@ -130,6 +142,55 @@ public function fetch(Tenant $tenant, Policy $policy, ?string $actorEmail = null
];
}
private function isMetadataOnlyPolicyType(string $policyType): bool
{
foreach (config('tenantpilot.supported_policy_types', []) as $type) {
if (($type['type'] ?? null) === $policyType) {
return ($type['backup'] ?? null) === 'metadata-only';
}
}
return false;
}
/**
* @return array<int, string>
*/
private function metadataOnlySelect(string $policyType): array
{
$contract = $this->contracts->get($policyType);
$allowedSelect = $contract['allowed_select'] ?? [];
if (! is_array($allowedSelect)) {
return [];
}
return array_values(array_filter(
$allowedSelect,
static fn (mixed $key) => is_string($key) && $key !== '@odata.type'
));
}
private function filterMetadataOnlyPayload(string $policyType, array $payload): array
{
$contract = $this->contracts->get($policyType);
$allowedSelect = $contract['allowed_select'] ?? [];
if (! is_array($allowedSelect) || $allowedSelect === []) {
return $payload;
}
$filtered = [];
foreach ($allowedSelect as $key) {
if (is_string($key) && array_key_exists($key, $payload)) {
$filtered[$key] = $payload[$key];
}
}
return $filtered;
}
/**
* Hydrate settings catalog policies with configuration settings subresource.
*

View File

@ -323,11 +323,50 @@
'allowed_expand' => [],
'type_family' => [
'#microsoft.graph.mobileApp',
'#microsoft.graph.androidLobApp',
'#microsoft.graph.androidStoreApp',
'#microsoft.graph.androidManagedStoreApp',
'#microsoft.graph.iosLobApp',
'#microsoft.graph.iosStoreApp',
'#microsoft.graph.iosVppApp',
'#microsoft.graph.winGetApp',
'#microsoft.graph.macOSLobApp',
'#microsoft.graph.macOSMicrosoftEdgeApp',
'#microsoft.graph.macOSMicrosoftDefenderApp',
'#microsoft.graph.macOSDmgApp',
'#microsoft.graph.macOSPkgApp',
'#microsoft.graph.macOsVppApp',
'#microsoft.graph.macOSWebClip',
'#microsoft.graph.managedAndroidLobApp',
'#microsoft.graph.managedAndroidStoreApp',
'#microsoft.graph.managedIOSLobApp',
'#microsoft.graph.managedIOSStoreApp',
'#microsoft.graph.microsoftStoreForBusinessApp',
'#microsoft.graph.officeSuiteApp',
'#microsoft.graph.macOSOfficeSuiteApp',
'#microsoft.graph.webApp',
'#microsoft.graph.windowsWebApp',
'#microsoft.graph.windowsAppX',
'#microsoft.graph.windowsUniversalAppX',
'#microsoft.graph.windowsMicrosoftEdgeApp',
'#microsoft.graph.windowsMobileMSI',
'#microsoft.graph.windowsPhone81AppXBundle',
'#microsoft.graph.windowsPhone81AppX',
'#microsoft.graph.windowsPhone81StoreApp',
'#microsoft.graph.windowsPhoneXAP',
'#microsoft.graph.windowsStoreApp',
'#microsoft.graph.win32LobApp',
'#microsoft.graph.win32CatalogApp',
'#microsoft.graph.iOSiPadOSWebClip',
],
'create_method' => 'POST',
'update_method' => 'PATCH',
'id_field' => 'id',
'hydration' => 'properties',
'assignments_list_path' => '/deviceAppManagement/mobileApps/{id}/assignments',
'assignments_create_path' => '/deviceAppManagement/mobileApps/{id}/assign',
'assignments_create_method' => 'POST',
'assignments_payload_key' => 'mobileAppAssignments',
],
'assignmentFilter' => [
'resource' => 'deviceManagement/assignmentFilters',

View File

@ -102,6 +102,20 @@
->toContain('scheduledActionsForRule($expand=scheduledActionConfigurations)');
});
it('exposes mobile app assignment endpoints and type family', function () {
$contract = $this->registry->get('mobileApp');
expect($contract)->not->toBeEmpty();
expect($contract['assignments_list_path'] ?? null)
->toBe('/deviceAppManagement/mobileApps/{id}/assignments');
expect($contract['assignments_create_path'] ?? null)
->toBe('/deviceAppManagement/mobileApps/{id}/assign');
expect($contract['assignments_payload_key'] ?? null)
->toBe('mobileAppAssignments');
expect($this->registry->matchesTypeFamily('mobileApp', '#microsoft.graph.win32LobApp'))->toBeTrue();
expect($this->registry->matchesTypeFamily('mobileApp', '#microsoft.graph.iosVppApp'))->toBeTrue();
});
it('omits role scope tags from assignment filter selects', function () {
$contract = $this->registry->get('assignmentFilter');

View File

@ -24,6 +24,22 @@ public function getPolicy(string $policyType, string $policyId, array $options =
{
$this->requests[] = ['getPolicy', $policyType, $policyId, $options];
if ($policyType === 'mobileApp') {
return new GraphResponse(success: true, data: [
'payload' => [
'id' => $policyId,
'displayName' => 'Contoso Portal',
'publisher' => 'Contoso',
'description' => 'Company Portal',
'@odata.type' => '#microsoft.graph.win32LobApp',
'createdDateTime' => '2025-01-01T00:00:00Z',
'lastModifiedDateTime' => '2025-01-02T00:00:00Z',
'installCommandLine' => 'setup.exe /quiet',
'largeIcon' => ['type' => 'image/png', 'value' => '...'],
],
]);
}
return new GraphResponse(success: true, data: [
'payload' => [
'id' => $policyId,
@ -107,3 +123,45 @@ public function request(string $method, string $path, array $options = []): Grap
expect($client->requests[0][3]['expand'] ?? null)
->toBe('scheduledActionsForRule($expand=scheduledActionConfigurations)');
});
it('filters mobile app snapshots to metadata-only keys', function () {
$client = new PolicySnapshotGraphClient;
app()->instance(GraphClientInterface::class, $client);
$tenant = Tenant::factory()->create([
'tenant_id' => 'tenant-apps',
'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' => 'app-123',
'policy_type' => 'mobileApp',
'display_name' => 'Contoso Portal',
'platform' => 'all',
]);
$service = app(PolicySnapshotService::class);
$result = $service->fetch($tenant, $policy);
expect($result['payload'])->toHaveKeys([
'id',
'displayName',
'publisher',
'description',
'@odata.type',
'createdDateTime',
'lastModifiedDateTime',
]);
expect($result['payload'])->not->toHaveKey('installCommandLine');
expect($result['payload'])->not->toHaveKey('largeIcon');
expect($client->requests[0][0])->toBe('getPolicy');
expect($client->requests[0][1])->toBe('mobileApp');
expect($client->requests[0][2])->toBe('app-123');
expect($client->requests[0][3]['select'] ?? null)->toBeArray();
expect($client->requests[0][3]['select'])->toContain('displayName');
expect($client->requests[0][3]['select'])->not->toContain('@odata.type');
});