TenantAtlas/app/Services/Intune/VersionService.php
2025-12-23 00:09:03 +01:00

213 lines
7.4 KiB
PHP

<?php
namespace App\Services\Intune;
use App\Models\Policy;
use App\Models\PolicyVersion;
use App\Models\Tenant;
use App\Services\Graph\AssignmentFetcher;
use App\Services\Graph\AssignmentFilterResolver;
use App\Services\Graph\GroupResolver;
use App\Services\Graph\ScopeTagResolver;
use Carbon\CarbonImmutable;
class VersionService
{
public function __construct(
private readonly AuditLogger $auditLogger,
private readonly PolicySnapshotService $snapshotService,
private readonly AssignmentFetcher $assignmentFetcher,
private readonly GroupResolver $groupResolver,
private readonly AssignmentFilterResolver $assignmentFilterResolver,
private readonly ScopeTagResolver $scopeTagResolver,
) {}
public function captureVersion(
Policy $policy,
array $payload,
?string $createdBy = null,
array $metadata = [],
?array $assignments = null,
?array $scopeTags = null,
): PolicyVersion {
$versionNumber = $this->nextVersionNumber($policy);
$version = PolicyVersion::create([
'tenant_id' => $policy->tenant_id,
'policy_id' => $policy->id,
'version_number' => $versionNumber,
'policy_type' => $policy->policy_type,
'platform' => $policy->platform,
'created_by' => $createdBy,
'captured_at' => CarbonImmutable::now(),
'snapshot' => $payload,
'metadata' => $metadata,
'assignments' => $assignments,
'scope_tags' => $scopeTags,
'assignments_hash' => $assignments ? hash('sha256', json_encode($assignments)) : null,
'scope_tags_hash' => $scopeTags ? hash('sha256', json_encode($scopeTags)) : null,
]);
$this->auditLogger->log(
tenant: $policy->tenant,
action: 'policy.versioned',
context: [
'metadata' => [
'policy_id' => $policy->id,
'version_number' => $versionNumber,
],
],
actorEmail: $createdBy,
resourceType: 'policy',
resourceId: (string) $policy->id
);
return $version;
}
public function captureFromGraph(
Tenant $tenant,
Policy $policy,
?string $createdBy = null,
array $metadata = [],
bool $includeAssignments = true,
bool $includeScopeTags = true,
): PolicyVersion {
$graphOptions = $tenant->graphOptions();
$tenantIdentifier = $graphOptions['tenant'] ?? $tenant->graphTenantId() ?? (string) $tenant->getKey();
$snapshot = $this->snapshotService->fetch($tenant, $policy, $createdBy);
if (isset($snapshot['failure'])) {
$reason = $snapshot['failure']['reason'] ?? 'Unable to fetch policy snapshot';
throw new \RuntimeException($reason);
}
$payload = $snapshot['payload'];
$assignments = null;
$scopeTags = null;
$assignmentMetadata = [];
if ($includeAssignments) {
try {
$rawAssignments = $this->assignmentFetcher->fetch($tenantIdentifier, $policy->external_id, $graphOptions);
if (! empty($rawAssignments)) {
$resolvedGroups = [];
// Resolve groups
$groupIds = collect($rawAssignments)
->pluck('target.groupId')
->filter()
->unique()
->values()
->toArray();
if (! empty($groupIds)) {
$resolvedGroups = $this->groupResolver->resolveGroupIds($groupIds, $tenantIdentifier, $graphOptions);
}
$assignmentMetadata['has_orphaned_assignments'] = collect($resolvedGroups)
->contains(fn (array $group) => $group['orphaned'] ?? false);
$assignmentMetadata['assignments_count'] = count($rawAssignments);
$filterIds = collect($rawAssignments)
->pluck('target.deviceAndAppManagementAssignmentFilterId')
->filter()
->unique()
->values()
->all();
$filters = $this->assignmentFilterResolver->resolve($filterIds, $tenant);
$filterNames = collect($filters)
->pluck('displayName', 'id')
->all();
$assignments = $this->enrichAssignments($rawAssignments, $resolvedGroups, $filterNames);
}
} catch (\Throwable $e) {
$assignmentMetadata['assignments_fetch_failed'] = true;
$assignmentMetadata['assignments_fetch_error'] = $e->getMessage();
}
}
if ($includeScopeTags) {
$scopeTagIds = $payload['roleScopeTagIds'] ?? ['0'];
$scopeTags = $this->resolveScopeTags($tenant, $scopeTagIds);
}
$metadata = array_merge(
['source' => 'version_capture'],
$metadata,
$assignmentMetadata
);
return $this->captureVersion(
policy: $policy,
payload: $payload,
createdBy: $createdBy,
metadata: $metadata,
assignments: $assignments,
scopeTags: $scopeTags,
);
}
/**
* @param array<int, array<string, mixed>> $assignments
* @param array<string, array{id:string,displayName:?string,orphaned:bool}> $groups
* @param array<string, string> $filterNames
* @return array<int, array<string, mixed>>
*/
private function enrichAssignments(array $assignments, array $groups, array $filterNames): array
{
return array_map(function (array $assignment) use ($groups, $filterNames): array {
$target = $assignment['target'] ?? [];
$groupId = $target['groupId'] ?? null;
if ($groupId && isset($groups[$groupId])) {
$target['group_display_name'] = $groups[$groupId]['displayName'] ?? null;
$target['group_orphaned'] = $groups[$groupId]['orphaned'] ?? false;
}
$filterId = $target['deviceAndAppManagementAssignmentFilterId'] ?? null;
if ($filterId && isset($filterNames[$filterId])) {
$target['assignment_filter_name'] = $filterNames[$filterId];
}
$assignment['target'] = $target;
return $assignment;
}, $assignments);
}
/**
* @param array<int, string> $scopeTagIds
* @return array{ids:array<int, string>,names:array<int, string>}
*/
private function resolveScopeTags(Tenant $tenant, array $scopeTagIds): array
{
$scopeTags = $this->scopeTagResolver->resolve($scopeTagIds, $tenant);
$names = [];
foreach ($scopeTagIds as $id) {
$scopeTag = collect($scopeTags)->firstWhere('id', $id);
$names[] = $scopeTag['displayName'] ?? ($id === '0' ? 'Default' : "Unknown (ID: {$id})");
}
return [
'ids' => $scopeTagIds,
'names' => $names,
];
}
private function nextVersionNumber(Policy $policy): int
{
$current = PolicyVersion::query()
->where('policy_id', $policy->id)
->max('version_number');
return (int) ($current ?? 0) + 1;
}
}