|null $canonicalTypes */ public function start( ManagedEnvironment $tenant, ProviderConnection $providerConnection, User $actor, ?array $canonicalTypes = null, ): OperationRun { $this->authorize($tenant, $actor); $this->assertProviderConnectionInScope($tenant, $providerConnection); $resourceTypes = $this->normalizeResourceTypes($canonicalTypes); $context = $this->runContext($tenant, $providerConnection, $resourceTypes); $run = $this->operationRuns->ensureRunWithIdentity( tenant: $tenant, type: OperationRunType::TenantConfigurationCapture->value, identityInputs: [ 'provider_connection_id' => (int) $providerConnection->getKey(), 'resource_types' => $resourceTypes, ], context: $context, initiator: $actor, ); if (! $run->wasRecentlyCreated) { return $run; } $this->recordAudit( action: 'tenant_configuration.capture.started', run: $run, tenant: $tenant, providerConnection: $providerConnection, actor: $actor, outcome: AuditOutcome::Info, metadata: ['resource_types' => $resourceTypes], ); try { $this->operationRuns->dispatchOrFail( $run, fn () => CaptureTenantConfigurationEvidenceJob::dispatch($run), emitQueuedNotification: false, ); } catch (Throwable $e) { $this->recordAudit( action: 'tenant_configuration.capture.failed', run: $run->fresh() ?? $run, tenant: $tenant, providerConnection: $providerConnection, actor: $actor, outcome: AuditOutcome::Failed, metadata: [ 'reason_code' => 'dispatch_failed', 'message' => RunFailureSanitizer::sanitizeMessage($e->getMessage()), 'resource_types' => $resourceTypes, ], ); throw $e; } return $run; } private function authorize(ManagedEnvironment $tenant, User $actor): void { $decision = $this->accessScopeResolver->decision($actor, $tenant, Capabilities::EVIDENCE_MANAGE); if ($decision->allowed()) { return; } if ($decision->shouldDenyAsNotFound()) { throw new NotFoundHttpException('Managed environment not found.'); } throw (new AuthorizationException('This action is unauthorized.'))->withStatus(403); } private function assertProviderConnectionInScope(ManagedEnvironment $tenant, ProviderConnection $providerConnection): void { if ((int) $providerConnection->managed_environment_id !== (int) $tenant->getKey() || (int) $providerConnection->workspace_id !== (int) $tenant->workspace_id ) { throw new NotFoundHttpException('Provider connection not found.'); } } /** * @param list|null $canonicalTypes * @return list */ private function normalizeResourceTypes(?array $canonicalTypes): array { $types = $canonicalTypes ?? ResourceTypeRegistry::defaultCanonicalTypes(); return collect($types) ->filter(static fn (mixed $type): bool => is_string($type) && trim($type) !== '') ->map(static fn (string $type): string => trim($type)) ->unique() ->sort() ->values() ->all(); } /** * @param list $resourceTypes * @return array */ private function runContext(ManagedEnvironment $tenant, ProviderConnection $providerConnection, array $resourceTypes): array { return [ 'operation' => [ 'type' => OperationRunType::TenantConfigurationCapture->value, ], 'target_scope' => [ 'workspace_id' => (int) $tenant->workspace_id, 'managed_environment_id' => (int) $tenant->getKey(), 'provider_connection_id' => (int) $providerConnection->getKey(), ], 'resource_types' => $resourceTypes, 'required_capability' => Capabilities::EVIDENCE_MANAGE, 'execution_authority_mode' => ExecutionAuthorityMode::ActorBound->value, ]; } /** * @param array $metadata */ private function recordAudit( string $action, OperationRun $run, ManagedEnvironment $tenant, ProviderConnection $providerConnection, User $actor, AuditOutcome $outcome, array $metadata = [], ): void { $this->auditRecorder->record( action: $action, context: [ 'metadata' => [ 'operation_run_id' => (int) $run->getKey(), 'provider_connection_id' => (int) $providerConnection->getKey(), ...$metadata, ], ], workspace: $tenant->workspace, tenant: $tenant, actor: AuditActorSnapshot::human($actor), target: new AuditTargetSnapshot('operation_run', (int) $run->getKey()), outcome: $outcome, operationRunId: (int) $run->getKey(), ); } }