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

195 lines
6.8 KiB
PHP

<?php
namespace App\Services;
use App\Models\BackupItem;
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 Illuminate\Support\Facades\Log;
class AssignmentBackupService
{
public function __construct(
private readonly AssignmentFetcher $assignmentFetcher,
private readonly GroupResolver $groupResolver,
private readonly AssignmentFilterResolver $assignmentFilterResolver,
private readonly ScopeTagResolver $scopeTagResolver,
) {}
/**
* Enrich a backup item with assignments and scope tag metadata.
*
* @param BackupItem $backupItem The backup item to enrich
* @param Tenant $tenant Tenant model with credentials
* @param string $policyId Policy ID (external_id from Graph)
* @param array $policyPayload Full policy payload from Graph
* @param bool $includeAssignments Whether to fetch and include assignments
* @return BackupItem Updated backup item with assignments and metadata
*/
public function enrichWithAssignments(
BackupItem $backupItem,
Tenant $tenant,
string $policyId,
array $policyPayload,
bool $includeAssignments = false
): BackupItem {
// Extract scope tags from payload (always available in policy)
$scopeTagIds = $policyPayload['roleScopeTagIds'] ?? ['0'];
$scopeTagNames = $this->resolveScopeTagNames($scopeTagIds, $tenant);
$metadata = $backupItem->metadata ?? [];
$metadata['scope_tag_ids'] = $scopeTagIds;
$metadata['scope_tag_names'] = $scopeTagNames;
// Only fetch assignments if explicitly requested
if (! $includeAssignments) {
$metadata['assignment_count'] = 0;
$backupItem->update([
'assignments' => null,
'metadata' => $metadata,
]);
return $backupItem->refresh();
}
// Fetch assignments from Graph API
$graphOptions = $tenant->graphOptions();
$tenantId = $graphOptions['tenant'] ?? $tenant->external_id ?? $tenant->tenant_id;
$assignments = $this->assignmentFetcher->fetch($tenantId, $policyId, $graphOptions);
if (empty($assignments)) {
// No assignments or fetch failed
$metadata['assignment_count'] = 0;
$metadata['assignments_fetch_failed'] = true;
$metadata['has_orphaned_assignments'] = false;
$backupItem->update([
'assignments' => [], // Return empty array instead of null
'metadata' => $metadata,
]);
Log::warning('No assignments fetched for policy', [
'tenant_id' => $tenantId,
'policy_id' => $policyId,
'backup_item_id' => $backupItem->id,
]);
return $backupItem->refresh();
}
// Extract group IDs and resolve for orphan detection
$groupIds = $this->extractGroupIds($assignments);
$resolvedGroups = [];
$hasOrphanedGroups = false;
if (! empty($groupIds)) {
$resolvedGroups = $this->groupResolver->resolveGroupIds($groupIds, $tenantId, $graphOptions);
$hasOrphanedGroups = collect($resolvedGroups)
->contains(fn (array $group) => $group['orphaned'] ?? false);
}
$filterIds = collect($assignments)
->pluck('target.deviceAndAppManagementAssignmentFilterId')
->filter()
->unique()
->values()
->all();
$filters = $this->assignmentFilterResolver->resolve($filterIds, $tenant);
$filterNames = collect($filters)
->pluck('displayName', 'id')
->all();
$assignments = $this->enrichAssignments($assignments, $resolvedGroups, $filterNames);
// Update backup item with assignments and metadata
$metadata['assignment_count'] = count($assignments);
$metadata['assignments_fetch_failed'] = false;
$metadata['has_orphaned_assignments'] = $hasOrphanedGroups;
$backupItem->update([
'assignments' => $assignments,
'metadata' => $metadata,
]);
Log::info('Assignments enriched for backup item', [
'tenant_id' => $tenantId,
'policy_id' => $policyId,
'backup_item_id' => $backupItem->id,
'assignment_count' => count($assignments),
'has_orphaned' => $hasOrphanedGroups,
]);
return $backupItem->refresh();
}
/**
* Resolve scope tag IDs to display names.
*/
private function resolveScopeTagNames(array $scopeTagIds, Tenant $tenant): array
{
$scopeTags = $this->scopeTagResolver->resolve($scopeTagIds, $tenant);
$names = [];
foreach ($scopeTagIds as $id) {
$scopeTag = collect($scopeTags)->firstWhere('id', $id);
$names[] = $scopeTag['displayName'] ?? "Unknown (ID: {$id})";
}
return $names;
}
/**
* Extract group IDs from assignment array.
*/
private function extractGroupIds(array $assignments): array
{
$groupIds = [];
foreach ($assignments as $assignment) {
$target = $assignment['target'] ?? [];
$odataType = $target['@odata.type'] ?? '';
if (in_array($odataType, [
'#microsoft.graph.groupAssignmentTarget',
'#microsoft.graph.exclusionGroupAssignmentTarget',
], true) && isset($target['groupId'])) {
$groupIds[] = $target['groupId'];
}
}
return array_unique($groupIds);
}
/**
* @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);
}
}