TenantAtlas/apps/platform/app/Services/TenantConfiguration/StartTenantConfigurationCapture.php
ahmido ca0f54614d feat: add generic content-backed coverage capture (#482)
Automated PR provided by Codex via Gitea API.

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #482
2026-06-25 19:55:52 +00:00

193 lines
6.5 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Services\TenantConfiguration;
use App\Jobs\TenantConfiguration\CaptureTenantConfigurationEvidenceJob;
use App\Models\ManagedEnvironment;
use App\Models\OperationRun;
use App\Models\ProviderConnection;
use App\Models\User;
use App\Services\Audit\AuditRecorder;
use App\Services\Auth\ManagedEnvironmentAccessScopeResolver;
use App\Services\OperationRunService;
use App\Support\Audit\AuditActorSnapshot;
use App\Support\Audit\AuditOutcome;
use App\Support\Audit\AuditTargetSnapshot;
use App\Support\Auth\Capabilities;
use App\Support\OpsUx\RunFailureSanitizer;
use App\Support\OperationRunType;
use App\Support\Operations\ExecutionAuthorityMode;
use Illuminate\Auth\Access\AuthorizationException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Throwable;
final class StartTenantConfigurationCapture
{
public function __construct(
private readonly ManagedEnvironmentAccessScopeResolver $accessScopeResolver,
private readonly OperationRunService $operationRuns,
private readonly AuditRecorder $auditRecorder,
) {}
/**
* @param list<string>|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<string>|null $canonicalTypes
* @return list<string>
*/
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<string> $resourceTypes
* @return array<string, mixed>
*/
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<string, mixed> $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(),
);
}
}