TenantAtlas/apps/platform/app/Services/TenantConfiguration/ExchangePowerShellIdentityEvidenceGate.php
ahmido a23131cdbc feat: add Exchange evidence capture adapter guard (#501)
Summary: add Spec 434 Exchange PowerShell evidence capture adapter and prerequisite/identity/content-only guards; cap Exchange evidence at content_backed while preserving Graph capture. Validation: php artisan test --filter=Spec434 --compact; ./vendor/bin/pint --dirty --test; git diff --cached --check. Product Surface: N/A - no rendered UI surface changed; no deployment impact.
Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #501
2026-07-08 10:46:05 +00:00

81 lines
2.9 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Services\TenantConfiguration;
use App\Models\ManagedEnvironment;
use App\Models\ProviderConnection;
use App\Models\TenantConfigurationResourceType;
use App\Support\TenantConfiguration\IdentityState;
final class ExchangePowerShellIdentityEvidenceGate
{
public const string FAILURE_CODE = 'exchange_capture_identity_blocked';
public function __construct(
private readonly CanonicalIdentityResolver $identityResolver,
private readonly CoverageResourceIdentityEvaluator $identityEvaluator,
) {}
/**
* @param list<array<string, mixed>> $items
* @param array<string, mixed> $sourceMetadata
* @return array{allowed: bool, failure_code?: string, reason_code?: string, blocked_index?: int, identity_state?: string, duplicate_identity?: bool}
*/
public function evaluateCollection(
ManagedEnvironment $tenant,
ProviderConnection $providerConnection,
TenantConfigurationResourceType $resourceType,
array $items,
array $sourceMetadata,
): array {
$seenKeys = [];
foreach ($items as $index => $item) {
$identity = $this->identityEvaluator->evaluate(
result: $this->identityResolver->resolve($resourceType, $item, $sourceMetadata),
tenant: $tenant,
providerConnection: $providerConnection,
resourceType: $resourceType,
);
if ($identity->identityState !== IdentityState::Stable) {
return [
'allowed' => false,
'failure_code' => self::FAILURE_CODE,
'reason_code' => $this->reasonCodeForIdentityState($identity->identityState),
'blocked_index' => $index,
'identity_state' => $identity->identityState->value,
];
}
if (isset($seenKeys[$identity->canonicalResourceKey])) {
return [
'allowed' => false,
'failure_code' => self::FAILURE_CODE,
'reason_code' => 'duplicate_stable_identity',
'blocked_index' => $index,
'identity_state' => IdentityState::IdentityConflict->value,
'duplicate_identity' => true,
];
}
$seenKeys[$identity->canonicalResourceKey] = true;
}
return ['allowed' => true];
}
private function reasonCodeForIdentityState(IdentityState $identityState): string
{
return match ($identityState) {
IdentityState::IdentityConflict => 'identity_conflict',
IdentityState::MissingExternalId => 'missing_stable_external_id',
IdentityState::UnsupportedIdentity => 'unsupported_identity',
IdentityState::Derived => 'derived_identity_blocked',
IdentityState::Stable => 'stable_identity',
};
}
}