## Summary - Adds the Spec 431 Exchange PowerShell invocation gate and operation registration slice. - Introduces trusted provider operation start handling and read-only Exchange PowerShell runner abstractions. - Updates provider capability/readiness evaluation and coverage for Exchange PowerShell invocation safety. ## Verification - `cd apps/platform && ./vendor/bin/sail artisan test tests/Feature/Providers/ProviderCapabilityEvaluationTest.php tests/Feature/Providers/ProviderOperationCapabilityGateTest.php tests/Feature/TenantConfiguration/Spec431ExchangePowerShellInvocationGateTest.php tests/Unit/Providers/ProviderCapabilityRegistryTest.php tests/Unit/Providers/ProviderOperationStartGateTest.php tests/Unit/Support/TenantConfiguration/Spec430ExchangePowerShellCommandAllowlistTest.php tests/Unit/Support/TenantConfiguration/Spec431ExchangePowerShellOperationRegistrationTest.php tests/Unit/Verification/ManagedEnvironmentPermissionCapabilityMappingTest.php` - Result: 80 passed, 685 assertions. ## Product Surface / Ops - Rendered UI surface changed: N/A. - Filament/Livewire surface changed: N/A. - Deployment impact: config/runtime service changes only; no migrations, queues, or assets added. Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #498
119 lines
5.0 KiB
PHP
119 lines
5.0 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Services\TenantConfiguration;
|
|
|
|
use App\Support\Providers\ProviderReasonCodes;
|
|
use RuntimeException;
|
|
|
|
final class FakeExchangePowerShellCommandRunner implements ExchangePowerShellCommandRunner
|
|
{
|
|
public const string SCENARIO_SUCCESS = 'success';
|
|
|
|
public const string SCENARIO_EMPTY_SUCCESS = 'empty_success';
|
|
|
|
public const string SCENARIO_AUTH_FAILURE = 'auth_failure';
|
|
|
|
public const string SCENARIO_AUTHORIZATION_FAILURE = 'authorization_failure';
|
|
|
|
public const string SCENARIO_TIMEOUT = 'timeout';
|
|
|
|
public const string SCENARIO_THROTTLED = 'throttled';
|
|
|
|
public const string SCENARIO_MODULE_UNAVAILABLE = 'module_unavailable';
|
|
|
|
public const string SCENARIO_COMMAND_UNAVAILABLE = 'command_unavailable';
|
|
|
|
public const string SCENARIO_MALFORMED_SCALAR = 'malformed_scalar';
|
|
|
|
public const string SCENARIO_MALFORMED_TEXT = 'malformed_text';
|
|
|
|
public const string SCENARIO_UNEXPECTED_EXCEPTION = 'unexpected_exception';
|
|
|
|
/**
|
|
* @var list<array<string, mixed>>
|
|
*/
|
|
public array $calls = [];
|
|
|
|
/**
|
|
* @param list<array<string, mixed>>|null $items
|
|
*/
|
|
public function __construct(
|
|
private readonly string $scenario = self::SCENARIO_SUCCESS,
|
|
private readonly ?array $items = null,
|
|
) {}
|
|
|
|
public function run(
|
|
ExchangePowerShellCommandContract $commandContract,
|
|
ExchangePowerShellInvocationContext $context,
|
|
): ExchangePowerShellInvocationResult {
|
|
$this->calls[] = [
|
|
'operation_run_id' => $context->operationRunId,
|
|
'canonical_type' => $context->canonicalType,
|
|
'command_name' => $context->commandName,
|
|
'runner_mode' => $context->runnerMode,
|
|
'parameter_names' => $commandContract->parameterNames(),
|
|
'context' => $context->toArray(),
|
|
];
|
|
|
|
return match ($this->scenario) {
|
|
self::SCENARIO_EMPTY_SUCCESS => ExchangePowerShellInvocationResult::succeeded([]),
|
|
self::SCENARIO_AUTH_FAILURE => ExchangePowerShellInvocationResult::failed(
|
|
reasonCode: ProviderReasonCodes::ProviderAuthFailed,
|
|
failureCode: 'execution_failed_authentication',
|
|
message: 'Exchange PowerShell authentication failed.',
|
|
),
|
|
self::SCENARIO_AUTHORIZATION_FAILURE => ExchangePowerShellInvocationResult::failed(
|
|
reasonCode: ProviderReasonCodes::ProviderPermissionDenied,
|
|
failureCode: 'execution_failed_authorization',
|
|
message: 'Exchange PowerShell authorization failed.',
|
|
),
|
|
self::SCENARIO_TIMEOUT => ExchangePowerShellInvocationResult::failed(
|
|
reasonCode: ProviderReasonCodes::NetworkUnreachable,
|
|
failureCode: 'execution_failed_timeout',
|
|
message: 'Exchange PowerShell invocation timed out.',
|
|
),
|
|
self::SCENARIO_THROTTLED => ExchangePowerShellInvocationResult::failed(
|
|
reasonCode: ProviderReasonCodes::RateLimited,
|
|
failureCode: 'execution_failed_throttled',
|
|
message: 'Exchange PowerShell invocation was rate limited.',
|
|
),
|
|
self::SCENARIO_MODULE_UNAVAILABLE => ExchangePowerShellInvocationResult::blocked(
|
|
reasonCode: ProviderReasonCodes::ProviderBindingUnsupported,
|
|
failureCode: 'execution_blocked_module_unavailable',
|
|
message: 'Exchange PowerShell module is unavailable.',
|
|
),
|
|
self::SCENARIO_COMMAND_UNAVAILABLE => ExchangePowerShellInvocationResult::blocked(
|
|
reasonCode: ProviderReasonCodes::ProviderBindingUnsupported,
|
|
failureCode: 'execution_blocked_command_unavailable',
|
|
message: 'Exchange PowerShell command is unavailable.',
|
|
),
|
|
self::SCENARIO_MALFORMED_SCALAR => ExchangePowerShellInvocationResult::succeededPayload(42),
|
|
self::SCENARIO_MALFORMED_TEXT => ExchangePowerShellInvocationResult::succeededPayload('raw transcript rejected by shape gate'),
|
|
self::SCENARIO_UNEXPECTED_EXCEPTION => throw new RuntimeException('Simulated runner exception with token and secret redaction pressure.'),
|
|
default => ExchangePowerShellInvocationResult::succeeded($this->items ?? $this->defaultItems($commandContract->toArray())),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $commandContract
|
|
* @return list<array<string, mixed>>
|
|
*/
|
|
private function defaultItems(array $commandContract): array
|
|
{
|
|
$canonicalType = (string) ($commandContract['canonical_type'] ?? 'exchange');
|
|
$label = (string) ($commandContract['human_label'] ?? 'Exchange object');
|
|
|
|
return [
|
|
[
|
|
'id' => 'spec431-'.$canonicalType.'-1',
|
|
'sourceId' => 'spec431-'.$canonicalType.'-source-1',
|
|
'Guid' => '00000000-0000-4000-8000-000000000431',
|
|
'Name' => 'Spec 431 '.$label,
|
|
'DisplayName' => 'Spec 431 '.$label,
|
|
],
|
|
];
|
|
}
|
|
}
|