feat(007): device config & compliance snapshot/restore improvements #9
@ -282,6 +282,7 @@ private function ignoredKeys(): array
|
|||||||
'settingCount',
|
'settingCount',
|
||||||
'settingsCount',
|
'settingsCount',
|
||||||
'templateReference',
|
'templateReference',
|
||||||
|
'scheduledActionsForRule',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -73,6 +73,16 @@ public function fetch(Tenant $tenant, Policy $policy, ?string $actorEmail = null
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($policy->policy_type === 'deviceCompliancePolicy') {
|
||||||
|
[$payload, $metadata] = $this->hydrateComplianceActions(
|
||||||
|
tenantIdentifier: $tenantIdentifier,
|
||||||
|
tenant: $tenant,
|
||||||
|
policyId: $policy->external_id,
|
||||||
|
payload: is_array($payload) ? $payload : [],
|
||||||
|
metadata: $metadata
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if ($response->failed()) {
|
if ($response->failed()) {
|
||||||
$reason = $response->warnings[0] ?? 'Graph request failed';
|
$reason = $response->warnings[0] ?? 'Graph request failed';
|
||||||
$failure = [
|
$failure = [
|
||||||
@ -174,6 +184,60 @@ private function hydrateSettingsCatalog(string $tenantIdentifier, Tenant $tenant
|
|||||||
return [$payload, $metadata];
|
return [$payload, $metadata];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hydrate compliance policies with scheduled actions (notification templates).
|
||||||
|
*
|
||||||
|
* @return array{0:array,1:array}
|
||||||
|
*/
|
||||||
|
private function hydrateComplianceActions(string $tenantIdentifier, Tenant $tenant, string $policyId, array $payload, array $metadata): array
|
||||||
|
{
|
||||||
|
$path = sprintf('deviceManagement/deviceCompliancePolicies/%s/scheduledActionsForRule', urlencode($policyId));
|
||||||
|
$options = [
|
||||||
|
'tenant' => $tenantIdentifier,
|
||||||
|
'client_id' => $tenant->app_client_id,
|
||||||
|
'client_secret' => $tenant->app_client_secret,
|
||||||
|
];
|
||||||
|
|
||||||
|
$actions = [];
|
||||||
|
$nextPath = $path;
|
||||||
|
$hydrationStatus = 'complete';
|
||||||
|
|
||||||
|
while ($nextPath) {
|
||||||
|
$response = $this->graphClient->request('GET', $nextPath, $options);
|
||||||
|
|
||||||
|
if ($response->failed()) {
|
||||||
|
$hydrationStatus = 'failed';
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = $response->data;
|
||||||
|
$pageItems = $data['value'] ?? (is_array($data) ? $data : []);
|
||||||
|
|
||||||
|
foreach ($pageItems as $item) {
|
||||||
|
if (is_array($item)) {
|
||||||
|
$actions[] = $item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$nextLink = $data['@odata.nextLink'] ?? null;
|
||||||
|
|
||||||
|
if (! $nextLink) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$nextPath = $this->stripGraphBaseUrl((string) $nextLink);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! empty($actions)) {
|
||||||
|
$payload['scheduledActionsForRule'] = $actions;
|
||||||
|
}
|
||||||
|
|
||||||
|
$metadata['compliance_actions_hydration'] = $hydrationStatus;
|
||||||
|
|
||||||
|
return [$payload, $metadata];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extract all settingDefinitionId from settings array, including nested children.
|
* Extract all settingDefinitionId from settings array, including nested children.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -15,6 +15,14 @@
|
|||||||
'bitLockerEnabled' => false,
|
'bitLockerEnabled' => false,
|
||||||
'osMinimumVersion' => '10.0.19045',
|
'osMinimumVersion' => '10.0.19045',
|
||||||
'activeFirewallRequired' => true,
|
'activeFirewallRequired' => true,
|
||||||
|
'scheduledActionsForRule' => [
|
||||||
|
[
|
||||||
|
'ruleName' => 'Default rule',
|
||||||
|
'scheduledActionConfigurations' => [
|
||||||
|
['actionType' => 'notification'],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
'customSetting' => 'Custom value',
|
'customSetting' => 'Custom value',
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -31,6 +39,8 @@
|
|||||||
expect($additionalBlock)->not->toBeNull();
|
expect($additionalBlock)->not->toBeNull();
|
||||||
expect(collect($additionalBlock['rows'])->pluck('label')->all())
|
expect(collect($additionalBlock['rows'])->pluck('label')->all())
|
||||||
->toContain('Custom Setting');
|
->toContain('Custom Setting');
|
||||||
|
expect(collect($additionalBlock['rows'])->pluck('label')->all())
|
||||||
|
->not->toContain('Scheduled Actions For Rule');
|
||||||
|
|
||||||
expect($settings->pluck('title')->all())->not->toContain('General');
|
expect($settings->pluck('title')->all())->not->toContain('General');
|
||||||
});
|
});
|
||||||
|
|||||||
105
tests/Unit/PolicySnapshotServiceTest.php
Normal file
105
tests/Unit/PolicySnapshotServiceTest.php
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Models\Policy;
|
||||||
|
use App\Models\Tenant;
|
||||||
|
use App\Services\Graph\GraphClientInterface;
|
||||||
|
use App\Services\Graph\GraphResponse;
|
||||||
|
use App\Services\Intune\PolicySnapshotService;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
uses(TestCase::class);
|
||||||
|
uses(RefreshDatabase::class);
|
||||||
|
|
||||||
|
class PolicySnapshotGraphClient 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];
|
||||||
|
|
||||||
|
return new GraphResponse(success: true, data: [
|
||||||
|
'payload' => [
|
||||||
|
'id' => $policyId,
|
||||||
|
'displayName' => 'Compliance Alpha',
|
||||||
|
'@odata.type' => '#microsoft.graph.windows10CompliancePolicy',
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
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];
|
||||||
|
|
||||||
|
if (str_contains($path, 'scheduledActionsForRule')) {
|
||||||
|
return new GraphResponse(success: true, data: [
|
||||||
|
'value' => [
|
||||||
|
[
|
||||||
|
'ruleName' => 'Default rule',
|
||||||
|
'scheduledActionConfigurations' => [
|
||||||
|
[
|
||||||
|
'actionType' => 'notification',
|
||||||
|
'notificationTemplateId' => 'template-123',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new GraphResponse(success: true, data: []);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
it('hydrates compliance policy scheduled actions into snapshots', function () {
|
||||||
|
$client = new PolicySnapshotGraphClient;
|
||||||
|
app()->instance(GraphClientInterface::class, $client);
|
||||||
|
|
||||||
|
$tenant = Tenant::factory()->create([
|
||||||
|
'tenant_id' => 'tenant-compliance',
|
||||||
|
'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' => 'compliance-123',
|
||||||
|
'policy_type' => 'deviceCompliancePolicy',
|
||||||
|
'display_name' => 'Compliance Alpha',
|
||||||
|
'platform' => 'windows',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$service = app(PolicySnapshotService::class);
|
||||||
|
$result = $service->fetch($tenant, $policy);
|
||||||
|
|
||||||
|
expect($result)->toHaveKey('payload');
|
||||||
|
expect($result['payload'])->toHaveKey('scheduledActionsForRule');
|
||||||
|
expect($result['payload']['scheduledActionsForRule'])->toHaveCount(1);
|
||||||
|
expect($result['payload']['scheduledActionsForRule'][0]['scheduledActionConfigurations'][0]['notificationTemplateId'])
|
||||||
|
->toBe('template-123');
|
||||||
|
expect($result['metadata']['compliance_actions_hydration'])->toBe('complete');
|
||||||
|
expect($client->requests)->toContain(['getPolicy', 'deviceCompliancePolicy', 'compliance-123']);
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue
Block a user