operationRun = $operationRun; } /** * @return array */ public function middleware(): array { return [new TrackOperationRun]; } public function handle( MicrosoftProviderHealthCheck $healthCheck, OperationRunService $runs, ): void { $tenant = Tenant::query()->find($this->tenantId); if (! $tenant instanceof Tenant) { throw new RuntimeException('Tenant not found.'); } $user = User::query()->find($this->userId); if (! $user instanceof User) { throw new RuntimeException('User not found.'); } $connection = ProviderConnection::query() ->where('tenant_id', $tenant->getKey()) ->find($this->providerConnectionId); if (! $connection instanceof ProviderConnection) { throw new RuntimeException('ProviderConnection not found.'); } $result = $healthCheck->check($connection); $this->applyHealthResult($connection, $result); if (! $this->operationRun instanceof OperationRun) { return; } $entraTenantName = $this->resolveEntraTenantName($connection, $result); if ($entraTenantName !== null) { $metadata = is_array($connection->metadata) ? $connection->metadata : []; $metadata['entra_tenant_name'] = $entraTenantName; $connection->update(['metadata' => $metadata]); } $this->updateRunTargetScope($this->operationRun, $connection, $entraTenantName); $permissionService = app(TenantPermissionService::class); $graphOptions = null; if ($result->healthy) { try { $graphOptions = app(ProviderGateway::class)->graphOptions($connection); } catch (\Throwable) { $graphOptions = null; } } $permissionComparison = $result->healthy ? ($graphOptions === null ? $permissionService->compare( $tenant, persist: false, liveCheck: false, useConfiguredStub: false, ) : $permissionService->compare( $tenant, persist: true, liveCheck: true, useConfiguredStub: false, graphOptions: $graphOptions, )) : $permissionService->compare( $tenant, persist: false, liveCheck: false, useConfiguredStub: false, ); $permissionRows = $permissionComparison['permissions'] ?? []; $permissionRows = is_array($permissionRows) ? $permissionRows : []; $inventory = null; if (! $result->healthy) { $inventory = [ 'fresh' => false, 'reason_code' => $result->reasonCode ?? 'dependency_unreachable', 'message' => 'Provider connection check failed; permissions were not refreshed during this run.', ]; } elseif ($graphOptions === null) { $inventory = [ 'fresh' => false, 'reason_code' => 'provider_credential_missing', 'message' => 'Provider credentials were unavailable; observed permissions inventory was not refreshed during this run.', ]; } else { $liveCheck = $permissionComparison['live_check'] ?? null; $liveCheck = is_array($liveCheck) ? $liveCheck : []; $reasonCode = is_string($liveCheck['reason_code'] ?? null) ? (string) $liveCheck['reason_code'] : 'dependency_unreachable'; $appId = is_string($liveCheck['app_id'] ?? null) && $liveCheck['app_id'] !== '' ? (string) $liveCheck['app_id'] : null; $observedCount = is_numeric($liveCheck['observed_permissions_count'] ?? null) ? (int) $liveCheck['observed_permissions_count'] : null; $message = ($liveCheck['succeeded'] ?? false) === true ? 'Observed permissions inventory refreshed successfully.' : match ($reasonCode) { 'permissions_inventory_empty' => $appId !== null ? sprintf('No application permissions were detected for app id %s. Verify admin consent was granted for this exact app registration, then retry verification.', $appId) : 'No application permissions were detected. Verify admin consent was granted for the configured app registration, then retry verification.', default => 'Unable to refresh observed permissions inventory during this run. Retry verification.', }; $inventory = [ 'fresh' => ($liveCheck['succeeded'] ?? false) === true, 'reason_code' => $reasonCode, 'message' => $message, 'app_id' => $appId, 'observed_permissions_count' => $observedCount, ]; } $permissionChecks = TenantPermissionCheckClusters::buildChecks($tenant, $permissionRows, $inventory); $report = VerificationReportWriter::write( run: $this->operationRun, checks: [ [ 'key' => 'provider.connection.check', 'title' => 'Provider connection check', 'status' => $result->healthy ? 'pass' : 'fail', 'severity' => $result->healthy ? 'info' : 'critical', 'blocking' => ! $result->healthy, 'reason_code' => $result->healthy ? 'ok' : ($result->reasonCode ?? 'unknown_error'), 'message' => $result->healthy ? 'Connection is healthy.' : ($result->message ?? 'Health check failed.'), 'evidence' => array_values(array_filter([ [ 'kind' => 'provider_connection_id', 'value' => (int) $connection->getKey(), ], [ 'kind' => 'entra_tenant_id', 'value' => (string) $connection->entra_tenant_id, ], is_numeric($result->meta['http_status'] ?? null) ? [ 'kind' => 'http_status', 'value' => (int) $result->meta['http_status'], ] : null, is_string($result->meta['organization_id'] ?? null) ? [ 'kind' => 'organization_id', 'value' => (string) $result->meta['organization_id'], ] : null, ])), 'next_steps' => $result->healthy ? [] : [[ 'label' => 'Review provider connection', 'url' => \App\Filament\Resources\ProviderConnectionResource::getUrl('edit', [ 'record' => (int) $connection->getKey(), ], tenant: $tenant), ]], ], ...$permissionChecks, ], identity: [ 'provider_connection_id' => (int) $connection->getKey(), 'entra_tenant_id' => (string) $connection->entra_tenant_id, ], ); if ($result->healthy) { $run = $runs->updateRun( $this->operationRun, status: OperationRunStatus::Completed->value, outcome: OperationRunOutcome::Succeeded->value, ); $this->logVerificationCompletion($tenant, $user, $run, $report); return; } $run = $runs->updateRun( $this->operationRun, status: OperationRunStatus::Completed->value, outcome: OperationRunOutcome::Failed->value, failures: [[ 'code' => 'provider.connection.check.failed', 'reason_code' => $result->reasonCode ?? 'unknown_error', 'message' => $result->message ?? 'Health check failed.', ]], ); $this->logVerificationCompletion($tenant, $user, $run, $report); } private function resolveEntraTenantName(ProviderConnection $connection, HealthResult $result): ?string { $existing = Arr::get(is_array($connection->metadata) ? $connection->metadata : [], 'entra_tenant_name'); if (is_string($existing) && trim($existing) !== '') { return trim($existing); } $candidate = $result->meta['organization_display_name'] ?? null; return is_string($candidate) && trim($candidate) !== '' ? trim($candidate) : null; } private function updateRunTargetScope(OperationRun $run, ProviderConnection $connection, ?string $entraTenantName): void { $context = is_array($run->context) ? $run->context : []; $targetScope = $context['target_scope'] ?? []; $targetScope = is_array($targetScope) ? $targetScope : []; $targetScope['entra_tenant_id'] = $connection->entra_tenant_id; if (is_string($entraTenantName) && $entraTenantName !== '') { $targetScope['entra_tenant_name'] = $entraTenantName; } $context['target_scope'] = $targetScope; $run->update(['context' => $context]); } private function applyHealthResult(ProviderConnection $connection, HealthResult $result): void { $connection->update([ 'status' => $result->status, 'health_status' => $result->healthStatus, 'last_health_check_at' => now(), 'last_error_reason_code' => $result->healthy ? null : $result->reasonCode, 'last_error_message' => $result->healthy ? null : $result->message, ]); } /** * @param array $report */ private function logVerificationCompletion(Tenant $tenant, User $actor, OperationRun $run, array $report): void { $workspace = $tenant->workspace; if (! $workspace) { return; } $counts = $report['summary']['counts'] ?? []; $counts = is_array($counts) ? $counts : []; app(WorkspaceAuditLogger::class)->log( workspace: $workspace, action: AuditActionId::VerificationCompleted->value, context: [ 'metadata' => [ 'operation_run_id' => (int) $run->getKey(), 'counts' => $counts, ], ], actor: $actor, status: $run->outcome === OperationRunOutcome::Succeeded->value ? 'success' : 'failed', resourceType: 'operation_run', resourceId: (string) $run->getKey(), ); } }