467 lines
17 KiB
PHP
467 lines
17 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Models\RestoreRun;
|
|
use App\Models\Tenant;
|
|
use App\Services\Graph\GraphClientInterface;
|
|
use App\Services\Graph\GraphContractRegistry;
|
|
use App\Services\Graph\GraphLogger;
|
|
use App\Services\Graph\GraphResponse;
|
|
use App\Services\Intune\AuditLogger;
|
|
use Illuminate\Support\Arr;
|
|
use Illuminate\Support\Facades\Log;
|
|
|
|
class AssignmentRestoreService
|
|
{
|
|
public function __construct(
|
|
private readonly GraphClientInterface $graphClient,
|
|
private readonly GraphContractRegistry $contracts,
|
|
private readonly GraphLogger $graphLogger,
|
|
private readonly AuditLogger $auditLogger,
|
|
) {}
|
|
|
|
/**
|
|
* @param array<int, array<string, mixed>> $assignments
|
|
* @param array<string, string> $groupMapping
|
|
* @return array{outcomes: array<int, array<string, mixed>>, summary: array{success:int,failed:int,skipped:int}}
|
|
*/
|
|
public function restore(
|
|
Tenant $tenant,
|
|
string $policyType,
|
|
string $policyId,
|
|
array $assignments,
|
|
array $groupMapping,
|
|
?RestoreRun $restoreRun = null,
|
|
?string $actorEmail = null,
|
|
?string $actorName = null,
|
|
): array {
|
|
$outcomes = [];
|
|
$summary = [
|
|
'success' => 0,
|
|
'failed' => 0,
|
|
'skipped' => 0,
|
|
];
|
|
|
|
if ($assignments === []) {
|
|
return [
|
|
'outcomes' => $outcomes,
|
|
'summary' => $summary,
|
|
];
|
|
}
|
|
|
|
$contract = $this->contracts->get($policyType);
|
|
$createPath = $this->resolvePath($contract['assignments_create_path'] ?? null, $policyId);
|
|
$createMethod = strtoupper((string) ($contract['assignments_create_method'] ?? 'POST'));
|
|
$usesAssignAction = is_string($createPath) && str_ends_with($createPath, '/assign');
|
|
$listPath = $this->resolvePath($contract['assignments_list_path'] ?? null, $policyId);
|
|
$deletePathTemplate = $contract['assignments_delete_path'] ?? null;
|
|
|
|
if (! $createPath || (! $usesAssignAction && (! $listPath || ! $deletePathTemplate))) {
|
|
$outcomes[] = $this->failureOutcome(null, 'Assignments endpoints are not configured for this policy type.');
|
|
$summary['failed']++;
|
|
|
|
return [
|
|
'outcomes' => $outcomes,
|
|
'summary' => $summary,
|
|
];
|
|
}
|
|
|
|
$graphOptions = $tenant->graphOptions();
|
|
$tenantIdentifier = $graphOptions['tenant'] ?? $tenant->graphTenantId() ?? (string) $tenant->getKey();
|
|
|
|
$context = [
|
|
'tenant' => $tenantIdentifier,
|
|
'policy_id' => $policyId,
|
|
'policy_type' => $policyType,
|
|
'restore_run_id' => $restoreRun?->id,
|
|
];
|
|
|
|
$preparedAssignments = [];
|
|
$preparedMeta = [];
|
|
|
|
foreach ($assignments as $assignment) {
|
|
if (! is_array($assignment)) {
|
|
continue;
|
|
}
|
|
|
|
$groupId = $assignment['target']['groupId'] ?? null;
|
|
$mappedGroupId = $groupId && isset($groupMapping[$groupId]) ? $groupMapping[$groupId] : null;
|
|
|
|
if ($mappedGroupId === 'SKIP') {
|
|
$outcomes[] = $this->skipOutcome($assignment, $groupId, $mappedGroupId);
|
|
$summary['skipped']++;
|
|
$this->logAssignmentOutcome(
|
|
status: 'skipped',
|
|
tenant: $tenant,
|
|
assignment: $assignment,
|
|
restoreRun: $restoreRun,
|
|
actorEmail: $actorEmail,
|
|
actorName: $actorName,
|
|
metadata: [
|
|
'policy_id' => $policyId,
|
|
'policy_type' => $policyType,
|
|
'group_id' => $groupId,
|
|
'mapped_group_id' => $mappedGroupId,
|
|
]
|
|
);
|
|
|
|
continue;
|
|
}
|
|
|
|
$assignmentToRestore = $this->applyGroupMapping($assignment, $mappedGroupId);
|
|
$assignmentToRestore = $this->sanitizeAssignment($assignmentToRestore);
|
|
|
|
$preparedAssignments[] = $assignmentToRestore;
|
|
$preparedMeta[] = [
|
|
'assignment' => $assignment,
|
|
'group_id' => $groupId,
|
|
'mapped_group_id' => $mappedGroupId,
|
|
];
|
|
}
|
|
|
|
if ($preparedAssignments === []) {
|
|
return [
|
|
'outcomes' => $outcomes,
|
|
'summary' => $summary,
|
|
];
|
|
}
|
|
|
|
if ($usesAssignAction) {
|
|
$this->graphLogger->logRequest('restore_assignments_assign', $context + [
|
|
'method' => $createMethod,
|
|
'endpoint' => $createPath,
|
|
'assignments' => count($preparedAssignments),
|
|
]);
|
|
|
|
$assignResponse = $this->graphClient->request($createMethod, $createPath, [
|
|
'json' => ['assignments' => $preparedAssignments],
|
|
] + $graphOptions);
|
|
|
|
$this->graphLogger->logResponse('restore_assignments_assign', $assignResponse, $context + [
|
|
'method' => $createMethod,
|
|
'endpoint' => $createPath,
|
|
'assignments' => count($preparedAssignments),
|
|
]);
|
|
|
|
if ($assignResponse->successful()) {
|
|
foreach ($preparedMeta as $meta) {
|
|
$outcomes[] = $this->successOutcome(
|
|
$meta['assignment'],
|
|
$meta['group_id'],
|
|
$meta['mapped_group_id']
|
|
);
|
|
$summary['success']++;
|
|
$this->logAssignmentOutcome(
|
|
status: 'created',
|
|
tenant: $tenant,
|
|
assignment: $meta['assignment'],
|
|
restoreRun: $restoreRun,
|
|
actorEmail: $actorEmail,
|
|
actorName: $actorName,
|
|
metadata: [
|
|
'policy_id' => $policyId,
|
|
'policy_type' => $policyType,
|
|
'group_id' => $meta['group_id'],
|
|
'mapped_group_id' => $meta['mapped_group_id'],
|
|
]
|
|
);
|
|
}
|
|
} else {
|
|
$reason = $assignResponse->meta['error_message'] ?? 'Graph assign failed';
|
|
|
|
if ($preparedMeta === []) {
|
|
$outcomes[] = $this->failureOutcome(null, $reason, null, null, $assignResponse);
|
|
$summary['failed']++;
|
|
}
|
|
|
|
foreach ($preparedMeta as $meta) {
|
|
$outcomes[] = $this->failureOutcome(
|
|
$meta['assignment'],
|
|
$reason,
|
|
$meta['group_id'],
|
|
$meta['mapped_group_id'],
|
|
$assignResponse
|
|
);
|
|
$summary['failed']++;
|
|
$this->logAssignmentOutcome(
|
|
status: 'failed',
|
|
tenant: $tenant,
|
|
assignment: $meta['assignment'],
|
|
restoreRun: $restoreRun,
|
|
actorEmail: $actorEmail,
|
|
actorName: $actorName,
|
|
metadata: [
|
|
'policy_id' => $policyId,
|
|
'policy_type' => $policyType,
|
|
'group_id' => $meta['group_id'],
|
|
'mapped_group_id' => $meta['mapped_group_id'],
|
|
'graph_error_message' => $assignResponse->meta['error_message'] ?? null,
|
|
'graph_error_code' => $assignResponse->meta['error_code'] ?? null,
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
return [
|
|
'outcomes' => $outcomes,
|
|
'summary' => $summary,
|
|
];
|
|
}
|
|
|
|
$this->graphLogger->logRequest('restore_assignments_list', $context + [
|
|
'method' => 'GET',
|
|
'endpoint' => $listPath,
|
|
]);
|
|
|
|
$response = $this->graphClient->request('GET', $listPath, $graphOptions);
|
|
|
|
$this->graphLogger->logResponse('restore_assignments_list', $response, $context + [
|
|
'method' => 'GET',
|
|
'endpoint' => $listPath,
|
|
]);
|
|
|
|
$existingAssignments = $response->data['value'] ?? [];
|
|
|
|
foreach ($existingAssignments as $existing) {
|
|
$assignmentId = $existing['id'] ?? null;
|
|
|
|
if (! is_string($assignmentId) || $assignmentId === '') {
|
|
continue;
|
|
}
|
|
|
|
$deletePath = $this->resolvePath($deletePathTemplate, $policyId, $assignmentId);
|
|
|
|
if (! $deletePath) {
|
|
continue;
|
|
}
|
|
|
|
$this->graphLogger->logRequest('restore_assignments_delete', $context + [
|
|
'method' => 'DELETE',
|
|
'endpoint' => $deletePath,
|
|
'assignment_id' => $assignmentId,
|
|
]);
|
|
|
|
$deleteResponse = $this->graphClient->request('DELETE', $deletePath, $graphOptions);
|
|
|
|
$this->graphLogger->logResponse('restore_assignments_delete', $deleteResponse, $context + [
|
|
'method' => 'DELETE',
|
|
'endpoint' => $deletePath,
|
|
'assignment_id' => $assignmentId,
|
|
]);
|
|
|
|
if ($deleteResponse->failed()) {
|
|
Log::warning('Failed to delete existing assignment during restore', $context + [
|
|
'assignment_id' => $assignmentId,
|
|
'graph_error_message' => $deleteResponse->meta['error_message'] ?? null,
|
|
'graph_error_code' => $deleteResponse->meta['error_code'] ?? null,
|
|
]);
|
|
}
|
|
}
|
|
|
|
foreach ($preparedMeta as $index => $meta) {
|
|
$assignmentToRestore = $preparedAssignments[$index] ?? null;
|
|
|
|
if (! is_array($assignmentToRestore)) {
|
|
continue;
|
|
}
|
|
|
|
$this->graphLogger->logRequest('restore_assignments_create', $context + [
|
|
'method' => $createMethod,
|
|
'endpoint' => $createPath,
|
|
'group_id' => $meta['group_id'],
|
|
'mapped_group_id' => $meta['mapped_group_id'],
|
|
]);
|
|
|
|
$createResponse = $this->graphClient->request($createMethod, $createPath, [
|
|
'json' => $assignmentToRestore,
|
|
] + $graphOptions);
|
|
|
|
$this->graphLogger->logResponse('restore_assignments_create', $createResponse, $context + [
|
|
'method' => $createMethod,
|
|
'endpoint' => $createPath,
|
|
'group_id' => $meta['group_id'],
|
|
'mapped_group_id' => $meta['mapped_group_id'],
|
|
]);
|
|
|
|
if ($createResponse->successful()) {
|
|
$outcomes[] = $this->successOutcome($meta['assignment'], $meta['group_id'], $meta['mapped_group_id']);
|
|
$summary['success']++;
|
|
$this->logAssignmentOutcome(
|
|
status: 'created',
|
|
tenant: $tenant,
|
|
assignment: $meta['assignment'],
|
|
restoreRun: $restoreRun,
|
|
actorEmail: $actorEmail,
|
|
actorName: $actorName,
|
|
metadata: [
|
|
'policy_id' => $policyId,
|
|
'policy_type' => $policyType,
|
|
'group_id' => $meta['group_id'],
|
|
'mapped_group_id' => $meta['mapped_group_id'],
|
|
]
|
|
);
|
|
} else {
|
|
$outcomes[] = $this->failureOutcome(
|
|
$meta['assignment'],
|
|
$createResponse->meta['error_message'] ?? 'Graph create failed',
|
|
$meta['group_id'],
|
|
$meta['mapped_group_id'],
|
|
$createResponse
|
|
);
|
|
$summary['failed']++;
|
|
$this->logAssignmentOutcome(
|
|
status: 'failed',
|
|
tenant: $tenant,
|
|
assignment: $meta['assignment'],
|
|
restoreRun: $restoreRun,
|
|
actorEmail: $actorEmail,
|
|
actorName: $actorName,
|
|
metadata: [
|
|
'policy_id' => $policyId,
|
|
'policy_type' => $policyType,
|
|
'group_id' => $meta['group_id'],
|
|
'mapped_group_id' => $meta['mapped_group_id'],
|
|
'graph_error_message' => $createResponse->meta['error_message'] ?? null,
|
|
'graph_error_code' => $createResponse->meta['error_code'] ?? null,
|
|
],
|
|
);
|
|
}
|
|
|
|
usleep(100000);
|
|
}
|
|
|
|
return [
|
|
'outcomes' => $outcomes,
|
|
'summary' => $summary,
|
|
];
|
|
}
|
|
|
|
private function resolvePath(?string $template, string $policyId, ?string $assignmentId = null): ?string
|
|
{
|
|
if (! is_string($template) || $template === '') {
|
|
return null;
|
|
}
|
|
|
|
$path = str_replace('{id}', urlencode($policyId), $template);
|
|
|
|
if ($assignmentId !== null) {
|
|
$path = str_replace('{assignmentId}', urlencode($assignmentId), $path);
|
|
}
|
|
|
|
return $path;
|
|
}
|
|
|
|
private function applyGroupMapping(array $assignment, ?string $mappedGroupId): array
|
|
{
|
|
if (! $mappedGroupId) {
|
|
return $assignment;
|
|
}
|
|
|
|
$target = $assignment['target'] ?? [];
|
|
$odataType = $target['@odata.type'] ?? '';
|
|
|
|
if (in_array($odataType, [
|
|
'#microsoft.graph.groupAssignmentTarget',
|
|
'#microsoft.graph.exclusionGroupAssignmentTarget',
|
|
], true) && isset($target['groupId'])) {
|
|
$target['groupId'] = $mappedGroupId;
|
|
$assignment['target'] = $target;
|
|
}
|
|
|
|
return $assignment;
|
|
}
|
|
|
|
private function sanitizeAssignment(array $assignment): array
|
|
{
|
|
$assignment = Arr::except($assignment, ['id']);
|
|
$target = $assignment['target'] ?? [];
|
|
|
|
unset(
|
|
$target['group_display_name'],
|
|
$target['group_orphaned'],
|
|
$target['assignment_filter_name']
|
|
);
|
|
|
|
$assignment['target'] = $target;
|
|
|
|
return $assignment;
|
|
}
|
|
|
|
private function successOutcome(array $assignment, ?string $groupId, ?string $mappedGroupId): array
|
|
{
|
|
return [
|
|
'status' => 'success',
|
|
'assignment' => $this->sanitizeAssignment($assignment),
|
|
'group_id' => $groupId,
|
|
'mapped_group_id' => $mappedGroupId,
|
|
];
|
|
}
|
|
|
|
private function skipOutcome(array $assignment, ?string $groupId, ?string $mappedGroupId): array
|
|
{
|
|
return [
|
|
'status' => 'skipped',
|
|
'assignment' => $this->sanitizeAssignment($assignment),
|
|
'group_id' => $groupId,
|
|
'mapped_group_id' => $mappedGroupId,
|
|
];
|
|
}
|
|
|
|
private function failureOutcome(
|
|
?array $assignment,
|
|
string $reason,
|
|
?string $groupId = null,
|
|
?string $mappedGroupId = null,
|
|
?GraphResponse $response = null
|
|
): array {
|
|
return array_filter([
|
|
'status' => 'failed',
|
|
'assignment' => $assignment ? $this->sanitizeAssignment($assignment) : null,
|
|
'group_id' => $groupId,
|
|
'mapped_group_id' => $mappedGroupId,
|
|
'reason' => $reason,
|
|
'graph_error_message' => $response?->meta['error_message'] ?? null,
|
|
'graph_error_code' => $response?->meta['error_code'] ?? null,
|
|
'graph_request_id' => $response?->meta['request_id'] ?? null,
|
|
'graph_client_request_id' => $response?->meta['client_request_id'] ?? null,
|
|
], static fn ($value) => $value !== null);
|
|
}
|
|
|
|
private function logAssignmentOutcome(
|
|
string $status,
|
|
Tenant $tenant,
|
|
array $assignment,
|
|
?RestoreRun $restoreRun,
|
|
?string $actorEmail,
|
|
?string $actorName,
|
|
array $metadata
|
|
): void {
|
|
$action = match ($status) {
|
|
'created' => 'restore.assignment.created',
|
|
'failed' => 'restore.assignment.failed',
|
|
default => 'restore.assignment.skipped',
|
|
};
|
|
|
|
$statusLabel = match ($status) {
|
|
'created' => 'success',
|
|
'failed' => 'failed',
|
|
default => 'warning',
|
|
};
|
|
|
|
$this->auditLogger->log(
|
|
tenant: $tenant,
|
|
action: $action,
|
|
context: [
|
|
'metadata' => $metadata,
|
|
'assignment' => $this->sanitizeAssignment($assignment),
|
|
],
|
|
actorEmail: $actorEmail,
|
|
actorName: $actorName,
|
|
status: $statusLabel,
|
|
resourceType: 'restore_run',
|
|
resourceId: $restoreRun ? (string) $restoreRun->id : null
|
|
);
|
|
}
|
|
}
|