merge: agent session work
This commit is contained in:
commit
856fe9da60
@ -13,6 +13,7 @@
|
|||||||
use App\Services\Intune\ManagedDeviceAppConfigurationNormalizer;
|
use App\Services\Intune\ManagedDeviceAppConfigurationNormalizer;
|
||||||
use App\Services\Intune\ScriptsPolicyNormalizer;
|
use App\Services\Intune\ScriptsPolicyNormalizer;
|
||||||
use App\Services\Intune\SettingsCatalogPolicyNormalizer;
|
use App\Services\Intune\SettingsCatalogPolicyNormalizer;
|
||||||
|
use App\Services\Intune\TermsAndConditionsNormalizer;
|
||||||
use App\Services\Intune\WindowsDriverUpdateProfileNormalizer;
|
use App\Services\Intune\WindowsDriverUpdateProfileNormalizer;
|
||||||
use App\Services\Intune\WindowsFeatureUpdateProfileNormalizer;
|
use App\Services\Intune\WindowsFeatureUpdateProfileNormalizer;
|
||||||
use App\Services\Intune\WindowsQualityUpdateProfileNormalizer;
|
use App\Services\Intune\WindowsQualityUpdateProfileNormalizer;
|
||||||
@ -50,6 +51,7 @@ public function register(): void
|
|||||||
ManagedDeviceAppConfigurationNormalizer::class,
|
ManagedDeviceAppConfigurationNormalizer::class,
|
||||||
ScriptsPolicyNormalizer::class,
|
ScriptsPolicyNormalizer::class,
|
||||||
SettingsCatalogPolicyNormalizer::class,
|
SettingsCatalogPolicyNormalizer::class,
|
||||||
|
TermsAndConditionsNormalizer::class,
|
||||||
WindowsDriverUpdateProfileNormalizer::class,
|
WindowsDriverUpdateProfileNormalizer::class,
|
||||||
WindowsFeatureUpdateProfileNormalizer::class,
|
WindowsFeatureUpdateProfileNormalizer::class,
|
||||||
WindowsQualityUpdateProfileNormalizer::class,
|
WindowsQualityUpdateProfileNormalizer::class,
|
||||||
|
|||||||
94
app/Services/Intune/TermsAndConditionsNormalizer.php
Normal file
94
app/Services/Intune/TermsAndConditionsNormalizer.php
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services\Intune;
|
||||||
|
|
||||||
|
use Illuminate\Support\Arr;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
class TermsAndConditionsNormalizer implements PolicyTypeNormalizer
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly DefaultPolicyNormalizer $defaultNormalizer,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function supports(string $policyType): bool
|
||||||
|
{
|
||||||
|
return $policyType === 'termsAndConditions';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{status: string, settings: array<int, array<string, mixed>>, warnings: array<int, string>}
|
||||||
|
*/
|
||||||
|
public function normalize(?array $snapshot, string $policyType, ?string $platform = null): array
|
||||||
|
{
|
||||||
|
$snapshot = is_array($snapshot) ? $snapshot : [];
|
||||||
|
$entries = [];
|
||||||
|
|
||||||
|
$this->pushEntry($entries, 'Display name', Arr::get($snapshot, 'displayName'));
|
||||||
|
$this->pushEntry($entries, 'Title', Arr::get($snapshot, 'title'));
|
||||||
|
$this->pushEntry($entries, 'Description', Arr::get($snapshot, 'description'));
|
||||||
|
$this->pushEntry($entries, 'Acceptance statement', Arr::get($snapshot, 'acceptanceStatement'));
|
||||||
|
$this->pushEntry($entries, 'Body text', $this->limitText(Arr::get($snapshot, 'bodyText')));
|
||||||
|
$this->pushEntry($entries, 'Version', Arr::get($snapshot, 'version'));
|
||||||
|
|
||||||
|
$roleScopeTagIds = Arr::get($snapshot, 'roleScopeTagIds');
|
||||||
|
if (is_array($roleScopeTagIds) && $roleScopeTagIds !== []) {
|
||||||
|
$this->pushEntry($entries, 'Scope tag IDs', array_values($roleScopeTagIds));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($entries === []) {
|
||||||
|
return [
|
||||||
|
'status' => 'warning',
|
||||||
|
'settings' => [],
|
||||||
|
'warnings' => ['Terms & Conditions snapshot contains no readable fields.'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'status' => 'ok',
|
||||||
|
'settings' => [
|
||||||
|
[
|
||||||
|
'type' => 'keyValue',
|
||||||
|
'title' => 'Terms & Conditions',
|
||||||
|
'entries' => $entries,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'warnings' => [],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function flattenForDiff(?array $snapshot, string $policyType, ?string $platform = null): array
|
||||||
|
{
|
||||||
|
$normalized = $this->normalize($snapshot ?? [], $policyType, $platform);
|
||||||
|
|
||||||
|
return $this->defaultNormalizer->flattenNormalizedForDiff($normalized);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<int, array<string, mixed>> $entries
|
||||||
|
*/
|
||||||
|
private function pushEntry(array &$entries, string $key, mixed $value): void
|
||||||
|
{
|
||||||
|
if ($value === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_string($value) && $value === '') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$entries[] = [
|
||||||
|
'key' => $key,
|
||||||
|
'value' => $value,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function limitText(mixed $value): mixed
|
||||||
|
{
|
||||||
|
if (! is_string($value)) {
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Str::limit($value, 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -62,6 +62,10 @@ protected static function odataTypeMap(): array
|
|||||||
'windows' => '#microsoft.graph.deviceHealthScript',
|
'windows' => '#microsoft.graph.deviceHealthScript',
|
||||||
'all' => '#microsoft.graph.deviceHealthScript',
|
'all' => '#microsoft.graph.deviceHealthScript',
|
||||||
],
|
],
|
||||||
|
'termsAndConditions' => [
|
||||||
|
'windows' => '#microsoft.graph.termsAndConditions',
|
||||||
|
'all' => '#microsoft.graph.termsAndConditions',
|
||||||
|
],
|
||||||
'deviceComplianceScript' => [
|
'deviceComplianceScript' => [
|
||||||
'windows' => '#microsoft.graph.deviceComplianceScript',
|
'windows' => '#microsoft.graph.deviceComplianceScript',
|
||||||
'all' => '#microsoft.graph.deviceComplianceScript',
|
'all' => '#microsoft.graph.deviceComplianceScript',
|
||||||
|
|||||||
@ -584,6 +584,48 @@
|
|||||||
'assignments_create_method' => 'POST',
|
'assignments_create_method' => 'POST',
|
||||||
'assignments_payload_key' => 'enrollmentConfigurationAssignments',
|
'assignments_payload_key' => 'enrollmentConfigurationAssignments',
|
||||||
],
|
],
|
||||||
|
'termsAndConditions' => [
|
||||||
|
'resource' => 'deviceManagement/termsAndConditions',
|
||||||
|
'allowed_select' => [
|
||||||
|
'id',
|
||||||
|
'displayName',
|
||||||
|
'description',
|
||||||
|
'title',
|
||||||
|
'bodyText',
|
||||||
|
'acceptanceStatement',
|
||||||
|
'version',
|
||||||
|
'roleScopeTagIds',
|
||||||
|
'lastModifiedDateTime',
|
||||||
|
'createdDateTime',
|
||||||
|
],
|
||||||
|
'allowed_expand' => [],
|
||||||
|
'type_family' => [
|
||||||
|
'#microsoft.graph.termsAndConditions',
|
||||||
|
],
|
||||||
|
'create_method' => 'POST',
|
||||||
|
'update_method' => 'PATCH',
|
||||||
|
'id_field' => 'id',
|
||||||
|
'hydration' => 'properties',
|
||||||
|
'update_strip_keys' => [
|
||||||
|
'createdDateTime',
|
||||||
|
'lastModifiedDateTime',
|
||||||
|
'modifiedDateTime',
|
||||||
|
'version',
|
||||||
|
'acceptanceStatuses',
|
||||||
|
'assignments',
|
||||||
|
'groupAssignments',
|
||||||
|
],
|
||||||
|
'assignments_list_path' => '/deviceManagement/termsAndConditions/{id}/assignments',
|
||||||
|
'assignments_create_path' => '/deviceManagement/termsAndConditions/{id}/assignments',
|
||||||
|
'assignments_create_method' => 'POST',
|
||||||
|
'assignments_payload_key' => 'termsAndConditionsAssignments',
|
||||||
|
'assignments_update_path' => '/deviceManagement/termsAndConditions/{id}/assignments/{assignmentId}',
|
||||||
|
'assignments_update_method' => 'PATCH',
|
||||||
|
'assignments_delete_path' => '/deviceManagement/termsAndConditions/{id}/assignments/{assignmentId}',
|
||||||
|
'assignments_delete_method' => 'DELETE',
|
||||||
|
'supports_scope_tags' => true,
|
||||||
|
'scope_tag_field' => 'roleScopeTagIds',
|
||||||
|
],
|
||||||
'windowsAutopilotDeploymentProfile' => [
|
'windowsAutopilotDeploymentProfile' => [
|
||||||
'resource' => 'deviceManagement/windowsAutopilotDeploymentProfiles',
|
'resource' => 'deviceManagement/windowsAutopilotDeploymentProfiles',
|
||||||
'allowed_select' => ['id', 'displayName', 'description', '@odata.type', 'lastModifiedDateTime'],
|
'allowed_select' => ['id', 'displayName', 'description', '@odata.type', 'lastModifiedDateTime'],
|
||||||
|
|||||||
@ -226,6 +226,16 @@
|
|||||||
'restore' => 'preview-only',
|
'restore' => 'preview-only',
|
||||||
'risk' => 'high',
|
'risk' => 'high',
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
'type' => 'termsAndConditions',
|
||||||
|
'label' => 'Terms & Conditions',
|
||||||
|
'category' => 'Enrollment',
|
||||||
|
'platform' => 'all',
|
||||||
|
'endpoint' => 'deviceManagement/termsAndConditions',
|
||||||
|
'backup' => 'full',
|
||||||
|
'restore' => 'enabled',
|
||||||
|
'risk' => 'medium-high',
|
||||||
|
],
|
||||||
[
|
[
|
||||||
'type' => 'endpointSecurityIntent',
|
'type' => 'endpointSecurityIntent',
|
||||||
'label' => 'Endpoint Security Intents',
|
'label' => 'Endpoint Security Intents',
|
||||||
|
|||||||
218
tests/Feature/TermsAndConditionsPolicyTypeTest.php
Normal file
218
tests/Feature/TermsAndConditionsPolicyTypeTest.php
Normal file
@ -0,0 +1,218 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Models\Policy;
|
||||||
|
use App\Models\Tenant;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Services\Graph\GraphClientInterface;
|
||||||
|
use App\Services\Graph\GraphLogger;
|
||||||
|
use App\Services\Graph\GraphResponse;
|
||||||
|
use App\Services\Intune\PolicyNormalizer;
|
||||||
|
use App\Services\Intune\PolicySyncService;
|
||||||
|
use App\Services\Intune\RestoreService;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
|
||||||
|
use function Pest\Laravel\mock;
|
||||||
|
|
||||||
|
uses(RefreshDatabase::class);
|
||||||
|
|
||||||
|
class TermsAndConditionsRestoreGraphClient implements GraphClientInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var array<int, array{method:string,path:string,payload:array|null}>
|
||||||
|
*/
|
||||||
|
public array $requestCalls = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<int, GraphResponse> $requestResponses
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
private readonly GraphResponse $applyPolicyResponse,
|
||||||
|
private array $requestResponses = [],
|
||||||
|
) {}
|
||||||
|
|
||||||
|
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
|
||||||
|
{
|
||||||
|
return $this->applyPolicyResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getServicePrincipalPermissions(array $options = []): GraphResponse
|
||||||
|
{
|
||||||
|
return new GraphResponse(true, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function request(string $method, string $path, array $options = []): GraphResponse
|
||||||
|
{
|
||||||
|
$this->requestCalls[] = [
|
||||||
|
'method' => strtoupper($method),
|
||||||
|
'path' => $path,
|
||||||
|
'payload' => $options['json'] ?? null,
|
||||||
|
];
|
||||||
|
|
||||||
|
return array_shift($this->requestResponses) ?? new GraphResponse(true, []);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
it('includes terms and conditions policy type in supported types', function () {
|
||||||
|
$byType = collect(config('tenantpilot.supported_policy_types', []))
|
||||||
|
->keyBy('type');
|
||||||
|
|
||||||
|
expect($byType)->toHaveKey('termsAndConditions');
|
||||||
|
expect($byType['termsAndConditions']['endpoint'] ?? null)->toBe('deviceManagement/termsAndConditions');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('defines terms and conditions graph contract with assignments paths', function () {
|
||||||
|
$contract = config('graph_contracts.types.termsAndConditions');
|
||||||
|
|
||||||
|
expect($contract)->toBeArray();
|
||||||
|
expect($contract['resource'] ?? null)->toBe('deviceManagement/termsAndConditions');
|
||||||
|
expect($contract['assignments_list_path'] ?? null)->toBe('/deviceManagement/termsAndConditions/{id}/assignments');
|
||||||
|
expect($contract['assignments_payload_key'] ?? null)->toBe('termsAndConditionsAssignments');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('restores terms and conditions assignments via assignments endpoint', function () {
|
||||||
|
$client = new TermsAndConditionsRestoreGraphClient(
|
||||||
|
applyPolicyResponse: new GraphResponse(true, []),
|
||||||
|
requestResponses: [
|
||||||
|
new GraphResponse(true, ['value' => []]), // existing assignments list
|
||||||
|
new GraphResponse(true, []), // create assignments
|
||||||
|
],
|
||||||
|
);
|
||||||
|
app()->instance(GraphClientInterface::class, $client);
|
||||||
|
|
||||||
|
$tenant = Tenant::factory()->create(['tenant_id' => 'tenant-1']);
|
||||||
|
$policy = Policy::factory()->create([
|
||||||
|
'tenant_id' => $tenant->id,
|
||||||
|
'external_id' => 'tc-1',
|
||||||
|
'policy_type' => 'termsAndConditions',
|
||||||
|
'platform' => 'all',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$backupSet = \App\Models\BackupSet::factory()->create([
|
||||||
|
'tenant_id' => $tenant->id,
|
||||||
|
'status' => 'completed',
|
||||||
|
'item_count' => 1,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$backupItem = \App\Models\BackupItem::factory()->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' => [
|
||||||
|
'id' => $policy->external_id,
|
||||||
|
'@odata.type' => '#microsoft.graph.termsAndConditions',
|
||||||
|
],
|
||||||
|
'assignments' => [
|
||||||
|
[
|
||||||
|
'id' => 'assignment-1',
|
||||||
|
'intent' => 'apply',
|
||||||
|
'target' => [
|
||||||
|
'@odata.type' => '#microsoft.graph.groupAssignmentTarget',
|
||||||
|
'groupId' => 'source-group-1',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$user = User::factory()->create(['email' => 'tester@example.com']);
|
||||||
|
$this->actingAs($user);
|
||||||
|
|
||||||
|
$service = app(RestoreService::class);
|
||||||
|
$service->execute(
|
||||||
|
tenant: $tenant,
|
||||||
|
backupSet: $backupSet,
|
||||||
|
selectedItemIds: [$backupItem->id],
|
||||||
|
dryRun: false,
|
||||||
|
actorEmail: $user->email,
|
||||||
|
actorName: $user->name,
|
||||||
|
groupMapping: [
|
||||||
|
'source-group-1' => 'target-group-1',
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
$postCalls = collect($client->requestCalls)
|
||||||
|
->filter(fn (array $call) => $call['method'] === 'POST')
|
||||||
|
->values();
|
||||||
|
|
||||||
|
expect($postCalls)->toHaveCount(1);
|
||||||
|
expect($postCalls[0]['path'])->toBe('/deviceManagement/termsAndConditions/tc-1/assignments');
|
||||||
|
|
||||||
|
$payload = $postCalls[0]['payload'] ?? [];
|
||||||
|
expect($payload['target']['groupId'] ?? null)->toBe('target-group-1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('normalizes terms and conditions key fields', function () {
|
||||||
|
$normalized = app(PolicyNormalizer::class)->normalize([
|
||||||
|
'@odata.type' => '#microsoft.graph.termsAndConditions',
|
||||||
|
'displayName' => 'Terms and Conditions Alpha',
|
||||||
|
'title' => 'Alpha terms',
|
||||||
|
'description' => 'Long form description',
|
||||||
|
'acceptanceStatement' => 'I agree',
|
||||||
|
'bodyText' => str_repeat('Line.', 100),
|
||||||
|
'version' => 3,
|
||||||
|
'roleScopeTagIds' => ['0', '1'],
|
||||||
|
], 'termsAndConditions', 'all');
|
||||||
|
|
||||||
|
$entries = $normalized['settings'][0]['entries'] ?? [];
|
||||||
|
$byKey = collect($entries)->keyBy('key');
|
||||||
|
|
||||||
|
expect($byKey['Display name']['value'] ?? null)->toBe('Terms and Conditions Alpha');
|
||||||
|
expect($byKey['Title']['value'] ?? null)->toBe('Alpha terms');
|
||||||
|
expect($byKey['Acceptance statement']['value'] ?? null)->toBe('I agree');
|
||||||
|
expect($byKey['Version']['value'] ?? null)->toBe(3);
|
||||||
|
expect($byKey['Scope tag IDs']['value'] ?? null)->toBe(['0', '1']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('syncs terms and conditions from graph', function () {
|
||||||
|
$tenant = Tenant::factory()->create(['status' => 'active']);
|
||||||
|
$logger = mock(GraphLogger::class);
|
||||||
|
|
||||||
|
$logger->shouldReceive('logRequest')
|
||||||
|
->zeroOrMoreTimes()
|
||||||
|
->andReturnNull();
|
||||||
|
|
||||||
|
$logger->shouldReceive('logResponse')
|
||||||
|
->zeroOrMoreTimes()
|
||||||
|
->andReturnNull();
|
||||||
|
|
||||||
|
mock(GraphClientInterface::class)
|
||||||
|
->shouldReceive('listPolicies')
|
||||||
|
->once()
|
||||||
|
->with('termsAndConditions', mockery::type('array'))
|
||||||
|
->andReturn(new GraphResponse(
|
||||||
|
success: true,
|
||||||
|
data: [
|
||||||
|
[
|
||||||
|
'id' => 'tc-1',
|
||||||
|
'displayName' => 'T&C',
|
||||||
|
'@odata.type' => '#microsoft.graph.termsAndConditions',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
));
|
||||||
|
|
||||||
|
$service = app(PolicySyncService::class);
|
||||||
|
|
||||||
|
$service->syncPolicies($tenant, [
|
||||||
|
['type' => 'termsAndConditions', 'platform' => 'all'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(Policy::query()->where('tenant_id', $tenant->id)->where('policy_type', 'termsAndConditions')->count())
|
||||||
|
->toBe(1);
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue
Block a user