TenantAtlas/apps/platform/app/Services/TenantConfiguration/ExchangePowerShellProductionRunner.php
ahmido f4e342121a feat: add Exchange PowerShell production runner gate (#499)
Spec 432: Exchange PowerShell production runner boundary and runtime gate. Validation: php artisan test --filter=Spec432 --compact; ./vendor/bin/pint --dirty --test --format agent; git diff --check.

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #499
2026-07-07 18:34:18 +00:00

222 lines
9.2 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Services\TenantConfiguration;
use App\Models\ManagedEnvironment;
use App\Models\ProviderConnection;
use App\Support\Providers\ProviderReasonCodes;
use Illuminate\Support\Facades\Cache;
use Throwable;
final class ExchangePowerShellProductionRunner implements ExchangePowerShellCommandRunner
{
public function __construct(
private readonly ExchangePowerShellRuntimeReadinessChecker $runtimeReadiness,
private readonly ExchangePowerShellCredentialReferenceResolver $credentials,
private readonly ExchangePowerShellPermissionEvidenceEvaluator $permissions,
private readonly ExchangePowerShellProcessCommandBuilder $commands,
private readonly ExchangePowerShellProcessExecutor $executor,
private readonly ExchangePowerShellOutputGuard $outputGuard,
) {}
public function run(
ExchangePowerShellCommandContract $commandContract,
ExchangePowerShellInvocationContext $context,
): ExchangePowerShellInvocationResult {
if ($context->runnerMode !== ExchangePowerShellInvocationGate::RUNNER_MODE_PRODUCTION) {
return $this->blocked('execution_blocked_runner_mode_invalid', ['runner_mode' => $context->runnerMode]);
}
if (! (bool) config(ExchangePowerShellInvocationGate::PRODUCTION_RUNNER_CONFIG_PATH, false)) {
return $this->blocked('execution_blocked_production_runner_disabled', [
'runner_mode' => $context->runnerMode,
'execution_enabled' => false,
]);
}
$connection = ProviderConnection::query()
->whereKey($context->providerConnectionId)
->first();
$environment = ManagedEnvironment::query()
->whereKey($context->managedEnvironmentId)
->first();
if (! $connection instanceof ProviderConnection || ! $environment instanceof ManagedEnvironment) {
return $this->blocked('scope_blocked_provider_connection_missing', ['scope_state' => 'missing']);
}
if ((int) $connection->workspace_id !== $context->workspaceId
|| (int) $connection->managed_environment_id !== $context->managedEnvironmentId
|| (int) $environment->workspace_id !== $context->workspaceId
) {
return $this->blocked('scope_blocked_provider_connection_mismatch', ['scope_state' => 'mismatch']);
}
$credentialGate = $this->credentials->resolve($connection);
if (! $credentialGate->allowed) {
return $this->blockedFromGate($credentialGate);
}
$permissionGate = $this->permissions->evaluate($environment, $connection);
if (! $permissionGate->allowed) {
return $this->blockedFromGate($permissionGate);
}
$runtimePolicy = ExchangePowerShellRuntimePolicy::fromConfig();
$runtimeGate = $this->runtimeReadiness->check($runtimePolicy);
if (! $runtimeGate->allowed) {
return $this->blockedFromGate($runtimeGate);
}
$build = $this->commands->build($commandContract, $runtimePolicy);
if (! $build->successful || ! $build->command instanceof ExchangePowerShellProcessCommand) {
return $this->blockedFromBuild($build);
}
$workspaceLock = Cache::lock('exchange-powershell:workspace:'.$context->workspaceId, $runtimePolicy->lockTtlSeconds);
$providerLock = Cache::lock('exchange-powershell:provider:'.$context->providerConnectionId, $runtimePolicy->lockTtlSeconds);
$workspaceAcquired = false;
$providerAcquired = false;
try {
$workspaceAcquired = $workspaceLock->get();
if (! $workspaceAcquired) {
return $this->blocked('execution_blocked_concurrency_workspace', [
'concurrency_state' => 'workspace_busy',
]);
}
$providerAcquired = $providerLock->get();
if (! $providerAcquired) {
return $this->blocked('execution_blocked_concurrency_provider', [
'concurrency_state' => 'provider_busy',
]);
}
$process = $this->executor->run($build->command);
$result = $this->outputGuard->guard($process, $commandContract, $runtimePolicy);
return $this->mergeResultContext($result, array_filter([
'runner_mode' => ExchangePowerShellInvocationGate::RUNNER_MODE_PRODUCTION,
'execution_enabled' => true,
'runtime_state' => $runtimeGate->context['runtime_state'] ?? null,
'credential_state' => $credentialGate->context['credential_state'] ?? null,
'permission_evidence_state' => $permissionGate->context['permission_evidence_state'] ?? null,
'command_builder_state' => $build->context['command_builder_state'] ?? null,
'concurrency_state' => 'released',
'duration_ms' => $result->context['duration_ms'] ?? null,
'item_count' => $result->context['item_count'] ?? null,
], static fn (mixed $value): bool => $value !== null && $value !== ''));
} catch (Throwable) {
return ExchangePowerShellInvocationResult::failed(
reasonCode: ProviderReasonCodes::UnknownError,
failureCode: 'execution_failed_unexpected_exception',
message: 'Exchange PowerShell production runner failed safely.',
context: [
'runner_mode' => ExchangePowerShellInvocationGate::RUNNER_MODE_PRODUCTION,
'execution_enabled' => true,
'failure_mode' => 'unexpected_exception',
'concurrency_state' => 'release_attempted',
],
);
} finally {
if ($providerAcquired) {
optional($providerLock)->release();
}
if ($workspaceAcquired) {
optional($workspaceLock)->release();
}
}
}
/**
* @param array<string, mixed> $context
*/
private function blocked(string $failureCode, array $context = []): ExchangePowerShellInvocationResult
{
return ExchangePowerShellInvocationResult::blocked(
reasonCode: ProviderReasonCodes::ProviderBindingUnsupported,
failureCode: $failureCode,
message: 'Exchange PowerShell production runner was blocked.',
context: $context,
);
}
private function blockedFromGate(ExchangePowerShellGateResult $gate): ExchangePowerShellInvocationResult
{
return ExchangePowerShellInvocationResult::blocked(
reasonCode: $gate->reasonCode ?? ProviderReasonCodes::ProviderBindingUnsupported,
failureCode: $gate->failureCode ?? 'execution_blocked_gate',
message: $gate->message ?? 'Exchange PowerShell production runner gate blocked execution.',
context: [
'runner_mode' => ExchangePowerShellInvocationGate::RUNNER_MODE_PRODUCTION,
'execution_enabled' => false,
...$gate->context,
],
);
}
private function blockedFromBuild(ExchangePowerShellProcessCommandBuilderResult $build): ExchangePowerShellInvocationResult
{
return ExchangePowerShellInvocationResult::blocked(
reasonCode: $build->reasonCode ?? ProviderReasonCodes::ProviderBindingUnsupported,
failureCode: $build->failureCode ?? 'command_contract_rejected',
message: $build->message ?? 'Exchange PowerShell command construction was rejected.',
context: [
'runner_mode' => ExchangePowerShellInvocationGate::RUNNER_MODE_PRODUCTION,
'execution_enabled' => false,
...$build->context,
],
);
}
/**
* @param array<string, mixed> $context
*/
private function mergeResultContext(ExchangePowerShellInvocationResult $result, array $context): ExchangePowerShellInvocationResult
{
if ($result->successful) {
return ExchangePowerShellInvocationResult::succeeded(
items: is_array($result->collection) ? $result->collection : [],
summaryCounts: $result->summaryCounts,
context: [
...$result->context,
...$context,
],
);
}
if ($result->blocked) {
return ExchangePowerShellInvocationResult::blocked(
reasonCode: $result->reasonCode ?? ProviderReasonCodes::ProviderBindingUnsupported,
failureCode: $result->failureCode ?? 'execution_blocked',
message: $result->message,
context: [
...$result->context,
...$context,
],
);
}
return ExchangePowerShellInvocationResult::failed(
reasonCode: $result->reasonCode ?? ProviderReasonCodes::UnknownError,
failureCode: $result->failureCode ?? 'execution_failed',
message: $result->message,
summaryCounts: $result->summaryCounts,
context: [
...$result->context,
...$context,
],
);
}
}