193 lines
6.5 KiB
PHP
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(),
|
|
);
|
|
}
|
|
}
|