TenantAtlas/apps/platform/tests/Feature/TenantConfiguration/Spec434ExchangeEvidenceCaptureAdapterTest.php
Ahmed Darrazi 6a6d9173e8
Some checks failed
PR Fast Feedback / fast-feedback (pull_request) Failing after 1m14s
feat: add Exchange evidence capture adapter guard
2026-07-08 12:38:38 +02:00

737 lines
31 KiB
PHP

<?php
declare(strict_types=1);
use App\Models\ManagedEnvironment;
use App\Models\ManagedEnvironmentPermission;
use App\Models\OperationRun;
use App\Models\ProviderConnection;
use App\Models\ProviderCredential;
use App\Models\TenantConfigurationResource;
use App\Models\TenantConfigurationResourceEvidence;
use App\Models\TenantConfigurationResourceType;
use App\Models\User;
use App\Services\TenantConfiguration\CanonicalIdentityResolver;
use App\Services\TenantConfiguration\ExchangePowerShellCommandContracts;
use App\Services\TenantConfiguration\ExchangePowerShellEvidenceCaptureAdapter;
use App\Services\TenantConfiguration\ExchangePowerShellIdentityEvidenceGate;
use App\Services\TenantConfiguration\ExchangePowerShellInvocationGate;
use App\Services\TenantConfiguration\ExchangePowerShellInvocationResult;
use App\Services\TenantConfiguration\ResourceTypeRegistry;
use App\Support\OperationRunOutcome;
use App\Support\OperationRunStatus;
use App\Support\OperationRunType;
use App\Support\Providers\ProviderConsentStatus;
use App\Support\Providers\ProviderCredentialKind;
use App\Support\Providers\ProviderCredentialSource;
use App\Support\Providers\ProviderVerificationStatus;
use App\Support\TenantConfiguration\CaptureOutcome;
use App\Support\TenantConfiguration\ClaimState;
use App\Support\TenantConfiguration\CoverageLevel;
use App\Support\TenantConfiguration\EvidenceState;
use App\Support\TenantConfiguration\IdentityState;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Schema;
beforeEach(function (): void {
app(ResourceTypeRegistry::class)->syncDefaults();
config([
ExchangePowerShellInvocationGate::FEATURE_CONFIG_PATH => true,
ExchangePowerShellInvocationGate::PRODUCTION_RUNNER_CONFIG_PATH => true,
'tenantpilot.exchange_powershell.credentials.supported_reference_kinds' => ['certificate'],
'tenantpilot.exchange_powershell.permission_evidence.currentness_days' => 30,
]);
});
it('Spec434 captures accepted Exchange PowerShell output as internal content-backed evidence only', function (): void {
[$user, $environment] = createMinimalUserWithTenant(role: 'owner');
$connection = spec434ReadyProviderConnection($environment);
spec434Credential($connection, ProviderCredentialKind::Certificate->value);
$run = spec434CaptureRun($environment, $user, $connection);
$result = app(ExchangePowerShellEvidenceCaptureAdapter::class)->capture(
tenant: $environment,
providerConnection: $connection,
operationRun: $run,
canonicalType: 'transportRule',
runnerResult: ExchangePowerShellInvocationResult::succeeded(
[spec434TransportRulePayload()],
context: ['output_state' => 'structured_collection'],
),
);
$run->refresh();
$resource = TenantConfigurationResource::query()->sole();
$evidence = TenantConfigurationResourceEvidence::query()->sole();
expect($result)
->toMatchArray([
'canonical_type' => 'transportRule',
'outcome' => CaptureOutcome::Captured->value,
'adapter_outcome' => CaptureOutcome::Captured->value,
'item_count' => 1,
'reason_code' => null,
'source_contract_key' => 'exchange_powershell.transportRule',
'summary_counts' => [
'total' => 1,
'processed' => 1,
'succeeded' => 1,
'failed' => 0,
'skipped' => 0,
'items' => 1,
],
])
->and($result['evidence_ids'])->toBe([(int) $evidence->getKey()])
->and($run->summary_counts)->toBe([
'total' => 1,
'processed' => 1,
'succeeded' => 1,
'failed' => 0,
'skipped' => 0,
'items' => 1,
])
->and($resource->workspace_id)->toBe((int) $environment->workspace_id)
->and($resource->managed_environment_id)->toBe((int) $environment->getKey())
->and($resource->provider_connection_id)->toBe((int) $connection->getKey())
->and($resource->latest_evidence_id)->toBe((int) $evidence->getKey())
->and($resource->latest_identity_state)->toBe(IdentityState::Stable)
->and($resource->latest_claim_state)->toBe(ClaimState::InternalOnly)
->and($resource->latest_evidence_state)->toBe(EvidenceState::ContentBacked)
->and($evidence->coverage_level)->toBe(CoverageLevel::ContentBacked)
->and($evidence->evidence_state)->toBe(EvidenceState::ContentBacked)
->and($evidence->capture_outcome)->toBe(CaptureOutcome::Captured)
->and($evidence->source_contract_key)->toBe('exchange_powershell.transportRule')
->and($evidence->source_endpoint)->toBe(ExchangePowerShellCommandContracts::SOURCE_SURFACE.':Get-TransportRule')
->and($evidence->source_metadata['capture_adapter'])->toBe('exchange_powershell_evidence_capture_adapter')
->and($evidence->source_metadata['content_level_maximum'])->toBe('content_backed')
->and($evidence->source_metadata['customer_claims_allowed'])->toBeFalse()
->and($evidence->source_metadata['restore_allowed'])->toBeFalse()
->and($evidence->source_metadata['certification_allowed'])->toBeFalse()
->and($evidence->normalized_payload['source']['source_surface'])->toBe(ExchangePowerShellCommandContracts::SOURCE_SURFACE)
->and($evidence->permission_context['provider_connection_id'])->toBe((int) $connection->getKey());
$persistedRun = json_encode([
'context' => $run->context,
'summary_counts' => $run->summary_counts,
'failure_summary' => $run->failure_summary,
], JSON_THROW_ON_ERROR);
expect($persistedRun)
->not->toContain('raw_shell_stdout')
->not->toContain('raw_shell_stderr')
->not->toContain('provider_response')
->not->toContain('client_secret')
->not->toContain('access_token');
});
it('Spec434 accepts only the included Exchange PowerShell capture targets', function (string $canonicalType): void {
[$user, $environment] = createMinimalUserWithTenant(role: 'owner');
$connection = spec434ReadyProviderConnection($environment);
spec434Credential($connection, ProviderCredentialKind::Certificate->value);
$run = spec434CaptureRun($environment, $user, $connection, resourceTypes: [$canonicalType]);
$result = app(ExchangePowerShellEvidenceCaptureAdapter::class)->capture(
tenant: $environment,
providerConnection: $connection,
operationRun: $run,
canonicalType: $canonicalType,
runnerResult: ExchangePowerShellInvocationResult::succeeded(
[spec434PayloadFor($canonicalType)],
context: ['output_state' => 'structured_collection'],
),
);
expect($result['outcome'])->toBe(CaptureOutcome::Captured->value)
->and($result['item_count'])->toBe(1)
->and(TenantConfigurationResourceEvidence::query()->count())->toBe(1);
})->with([
'transportRule',
'remoteDomain',
'inboundConnector',
]);
it('Spec434 blocks unsupported Exchange and Teams targets before evidence append', function (string $canonicalType): void {
[$user, $environment] = createMinimalUserWithTenant(role: 'owner');
$connection = spec434ReadyProviderConnection($environment);
spec434Credential($connection, ProviderCredentialKind::Certificate->value);
$run = spec434CaptureRun($environment, $user, $connection, resourceTypes: [$canonicalType]);
$result = app(ExchangePowerShellEvidenceCaptureAdapter::class)->capture(
tenant: $environment,
providerConnection: $connection,
operationRun: $run,
canonicalType: $canonicalType,
runnerResult: ExchangePowerShellInvocationResult::succeeded(
[['id' => 'unsupported-1', 'DisplayName' => 'Unsupported']],
context: ['output_state' => 'structured_collection'],
),
);
expect($result['adapter_outcome'])->toBe(ExchangePowerShellEvidenceCaptureAdapter::OUTCOME_PREREQUISITE_BLOCKED)
->and($result['reason_code'])->toBe('unsupported_exchange_capture_type')
->and(TenantConfigurationResource::query()->count())->toBe(0)
->and(TenantConfigurationResourceEvidence::query()->count())->toBe(0);
})->with([
'acceptedDomain',
'organizationConfig',
'meetingPolicy',
]);
it('Spec434 blocks missing operation runs through the typed adapter boundary', function (): void {
[, $environment] = createMinimalUserWithTenant(role: 'owner');
$connection = spec434ReadyProviderConnection($environment);
spec434Credential($connection, ProviderCredentialKind::Certificate->value);
expect(fn () => app(ExchangePowerShellEvidenceCaptureAdapter::class)->capture(
tenant: $environment,
providerConnection: $connection,
operationRun: null,
canonicalType: 'transportRule',
runnerResult: ExchangePowerShellInvocationResult::succeeded(
[spec434TransportRulePayload()],
context: ['output_state' => 'structured_collection'],
),
))->toThrow(TypeError::class);
expect(TenantConfigurationResource::query()->count())->toBe(0)
->and(TenantConfigurationResourceEvidence::query()->count())->toBe(0);
});
it('Spec434 blocks wrong operation type and same-scope mismatches before evidence append', function (string $case): void {
[$user, $environment] = createMinimalUserWithTenant(role: 'owner');
$connection = spec434ReadyProviderConnection($environment);
spec434Credential($connection, ProviderCredentialKind::Certificate->value);
$run = spec434CaptureRun($environment, $user, $connection);
if ($case === 'wrong_operation_type') {
$run->forceFill(['type' => OperationRunType::TenantConfigurationExchangePowerShellInvocation->value])->save();
}
if ($case === 'wrong_workspace_context') {
$run->forceFill(['context' => array_replace_recursive($run->context, [
'target_scope' => ['workspace_id' => (int) $environment->workspace_id + 999],
])])->save();
}
if ($case === 'wrong_managed_environment_context') {
$run->forceFill(['context' => array_replace_recursive($run->context, [
'target_scope' => ['managed_environment_id' => (int) $environment->getKey() + 999],
])])->save();
}
if ($case === 'wrong_provider_connection_context') {
$run->forceFill(['context' => array_replace_recursive($run->context, [
'target_scope' => ['provider_connection_id' => (int) $connection->getKey() + 999],
])])->save();
}
$result = app(ExchangePowerShellEvidenceCaptureAdapter::class)->capture(
tenant: $environment,
providerConnection: $connection,
operationRun: $run->refresh(),
canonicalType: 'transportRule',
runnerResult: ExchangePowerShellInvocationResult::succeeded(
[spec434TransportRulePayload()],
context: ['output_state' => 'structured_collection'],
),
);
expect($result['adapter_outcome'])->toBe(ExchangePowerShellEvidenceCaptureAdapter::OUTCOME_PREREQUISITE_BLOCKED)
->and($result['reason_code'])->toBe($case === 'wrong_operation_type' ? 'operation_type_mismatch' : 'scope_mismatch')
->and(TenantConfigurationResource::query()->count())->toBe(0)
->and(TenantConfigurationResourceEvidence::query()->count())->toBe(0);
})->with([
'wrong_operation_type',
'wrong_workspace_context',
'wrong_managed_environment_context',
'wrong_provider_connection_context',
]);
it('Spec434 blocks cross-scope provider connections before evidence append', function (): void {
[$user, $environment] = createMinimalUserWithTenant(role: 'owner');
[, $otherEnvironment] = createMinimalUserWithTenant(role: 'owner');
$foreignConnection = spec434ReadyProviderConnection($otherEnvironment);
spec434Credential($foreignConnection, ProviderCredentialKind::Certificate->value);
$run = spec434CaptureRun($environment, $user, $foreignConnection);
$result = app(ExchangePowerShellEvidenceCaptureAdapter::class)->capture(
tenant: $environment,
providerConnection: $foreignConnection,
operationRun: $run,
canonicalType: 'transportRule',
runnerResult: ExchangePowerShellInvocationResult::succeeded(
[spec434TransportRulePayload()],
context: ['output_state' => 'structured_collection'],
),
);
expect($result['adapter_outcome'])->toBe(ExchangePowerShellEvidenceCaptureAdapter::OUTCOME_PREREQUISITE_BLOCKED)
->and($result['reason_code'])->toBe('scope_mismatch')
->and(TenantConfigurationResource::query()->count())->toBe(0)
->and(TenantConfigurationResourceEvidence::query()->count())->toBe(0);
});
it('Spec434 blocks missing readiness and non-supported provider capability before evidence append', function (string $case): void {
[$user, $environment] = createMinimalUserWithTenant(role: 'owner');
$connection = match ($case) {
'missing_readiness' => spec434ReadyProviderConnection($environment, seedExchangePowerShellPermission: false),
'capability_not_supported' => spec434ReadyProviderConnection($environment, [
'consent_status' => ProviderConsentStatus::Required->value,
'consent_granted_at' => null,
]),
default => throw new InvalidArgumentException('Unsupported Spec434 readiness case.'),
};
spec434Credential($connection, ProviderCredentialKind::Certificate->value);
$run = spec434CaptureRun($environment, $user, $connection);
$result = app(ExchangePowerShellEvidenceCaptureAdapter::class)->capture(
tenant: $environment,
providerConnection: $connection,
operationRun: $run,
canonicalType: 'transportRule',
runnerResult: ExchangePowerShellInvocationResult::succeeded(
[spec434TransportRulePayload()],
context: ['output_state' => 'structured_collection'],
),
);
expect($result['adapter_outcome'])->toBe(ExchangePowerShellEvidenceCaptureAdapter::OUTCOME_PREREQUISITE_BLOCKED)
->and($result['reason_code'])->not->toBeNull()
->and(TenantConfigurationResource::query()->count())->toBe(0)
->and(TenantConfigurationResourceEvidence::query()->count())->toBe(0);
})->with([
'missing_readiness',
'capability_not_supported',
]);
it('Spec434 blocks runner results that are not accepted structured or empty collections', function (ExchangePowerShellInvocationResult $runnerResult): void {
[$user, $environment] = createMinimalUserWithTenant(role: 'owner');
$connection = spec434ReadyProviderConnection($environment);
spec434Credential($connection, ProviderCredentialKind::Certificate->value);
$run = spec434CaptureRun($environment, $user, $connection);
$result = app(ExchangePowerShellEvidenceCaptureAdapter::class)->capture(
tenant: $environment,
providerConnection: $connection,
operationRun: $run,
canonicalType: 'transportRule',
runnerResult: $runnerResult,
);
expect($result['adapter_outcome'])->toBe(ExchangePowerShellEvidenceCaptureAdapter::OUTCOME_PREREQUISITE_BLOCKED)
->and(TenantConfigurationResource::query()->count())->toBe(0)
->and(TenantConfigurationResourceEvidence::query()->count())->toBe(0);
})->with([
'missing output state' => fn (): ExchangePowerShellInvocationResult => ExchangePowerShellInvocationResult::succeeded([spec434TransportRulePayload()]),
'associative payload' => fn (): ExchangePowerShellInvocationResult => ExchangePowerShellInvocationResult::succeededPayload(
['Guid' => 'not-a-list'],
['output_state' => 'structured_collection'],
),
'failed runner' => fn (): ExchangePowerShellInvocationResult => ExchangePowerShellInvocationResult::failed(
reasonCode: 'provider_failed',
failureCode: 'runner_failed',
message: 'Rejected safely.',
),
]);
it('Spec434 blocks unsafe identity before resource or evidence writes', function (string $canonicalType, array $payload, string $reasonCode): void {
[$user, $environment] = createMinimalUserWithTenant(role: 'owner');
$connection = spec434ReadyProviderConnection($environment);
spec434Credential($connection, ProviderCredentialKind::Certificate->value);
$run = spec434CaptureRun($environment, $user, $connection, resourceTypes: [$canonicalType]);
$result = app(ExchangePowerShellEvidenceCaptureAdapter::class)->capture(
tenant: $environment,
providerConnection: $connection,
operationRun: $run,
canonicalType: $canonicalType,
runnerResult: ExchangePowerShellInvocationResult::succeeded(
[$payload],
context: ['output_state' => 'structured_collection'],
),
);
expect($result['adapter_outcome'])->toBe(ExchangePowerShellEvidenceCaptureAdapter::OUTCOME_IDENTITY_BLOCKED)
->and($result['reason_code'])->toBe($reasonCode)
->and(TenantConfigurationResource::query()->count())->toBe(0)
->and(TenantConfigurationResourceEvidence::query()->count())->toBe(0);
})->with([
'transport rule display-name only' => [
'transportRule',
['DisplayName' => 'Display names are not stable identifiers'],
'missing_stable_external_id',
],
'remote domain derived-only domain name' => [
'remoteDomain',
['DomainName' => 'contoso.example', 'DisplayName' => 'Contoso'],
'missing_stable_external_id',
],
]);
it('Spec434 blocks duplicate stable identities before resource or evidence writes', function (): void {
[$user, $environment] = createMinimalUserWithTenant(role: 'owner');
$connection = spec434ReadyProviderConnection($environment);
spec434Credential($connection, ProviderCredentialKind::Certificate->value);
$run = spec434CaptureRun($environment, $user, $connection);
$result = app(ExchangePowerShellEvidenceCaptureAdapter::class)->capture(
tenant: $environment,
providerConnection: $connection,
operationRun: $run,
canonicalType: 'transportRule',
runnerResult: ExchangePowerShellInvocationResult::succeeded(
[
spec434TransportRulePayload(['DisplayName' => 'Duplicate A']),
spec434TransportRulePayload(['DisplayName' => 'Duplicate B']),
],
context: ['output_state' => 'structured_collection'],
),
);
expect($result['adapter_outcome'])->toBe(ExchangePowerShellEvidenceCaptureAdapter::OUTCOME_IDENTITY_BLOCKED)
->and($result['reason_code'])->toBe('duplicate_stable_identity')
->and(TenantConfigurationResource::query()->count())->toBe(0)
->and(TenantConfigurationResourceEvidence::query()->count())->toBe(0);
});
it('Spec434 identity gate detects existing same-scope identity conflict before writer use', function (): void {
[, $environment] = createMinimalUserWithTenant(role: 'owner');
$connection = spec434ReadyProviderConnection($environment);
$resourceType = app(ResourceTypeRegistry::class)->findActive('transportRule');
expect($resourceType)->toBeInstanceOf(TenantConfigurationResourceType::class);
$sourceMetadata = [
'source_contract_key' => 'exchange_powershell.transportRule',
'source_version' => ExchangePowerShellCommandContracts::COMMAND_CONTRACT_VERSION,
];
$payload = ['DisplayName' => 'Collision without stable id'];
$identity = app(CanonicalIdentityResolver::class)->resolve($resourceType, $payload, $sourceMetadata);
TenantConfigurationResource::factory()->create([
'workspace_id' => (int) $environment->workspace_id,
'managed_environment_id' => (int) $environment->getKey(),
'provider_connection_id' => (int) $connection->getKey(),
'resource_type_id' => (int) $resourceType->getKey(),
'canonical_type' => 'transportRule',
'canonical_resource_key' => $identity->canonicalResourceKey,
'source_identity' => $identity->sourceIdentity,
'secondary_identity_keys' => $identity->secondaryKeys,
'latest_identity_state' => IdentityState::MissingExternalId->value,
'latest_claim_state' => ClaimState::ClaimBlocked->value,
]);
$result = app(ExchangePowerShellIdentityEvidenceGate::class)->evaluateCollection(
tenant: $environment,
providerConnection: $connection,
resourceType: $resourceType,
items: [$payload],
sourceMetadata: $sourceMetadata,
);
$resource = TenantConfigurationResource::query()->sole();
expect($result['allowed'])->toBeFalse()
->and($result['reason_code'])->toBe('identity_conflict')
->and($resource->refresh()->latest_identity_state)->toBe(IdentityState::IdentityConflict)
->and(TenantConfigurationResourceEvidence::query()->count())->toBe(0);
});
it('Spec434 identity gate reports unsupported identity states without writer use', function (): void {
[, $environment] = createMinimalUserWithTenant(role: 'owner');
$connection = spec434ReadyProviderConnection($environment);
$resourceType = TenantConfigurationResourceType::factory()->create([
'canonical_type' => 'unsupportedExchangeIdentity',
]);
$result = app(ExchangePowerShellIdentityEvidenceGate::class)->evaluateCollection(
tenant: $environment,
providerConnection: $connection,
resourceType: $resourceType,
items: [['id' => 'unsupported-identity']],
sourceMetadata: [],
);
expect($result['allowed'])->toBeFalse()
->and($result['reason_code'])->toBe('unsupported_identity')
->and(TenantConfigurationResourceEvidence::query()->count())->toBe(0);
});
it('Spec434 empty collections persist only zero summary counts and no durable collection proof', function (): void {
[$user, $environment] = createMinimalUserWithTenant(role: 'owner');
$connection = spec434ReadyProviderConnection($environment);
spec434Credential($connection, ProviderCredentialKind::Certificate->value);
$run = spec434CaptureRun($environment, $user, $connection);
$result = app(ExchangePowerShellEvidenceCaptureAdapter::class)->capture(
tenant: $environment,
providerConnection: $connection,
operationRun: $run,
canonicalType: 'transportRule',
runnerResult: ExchangePowerShellInvocationResult::succeeded(
[],
context: ['output_state' => 'empty_collection'],
),
);
expect($result)
->toMatchArray([
'outcome' => CaptureOutcome::Captured->value,
'adapter_outcome' => ExchangePowerShellEvidenceCaptureAdapter::OUTCOME_EMPTY_COLLECTION,
'item_count' => 0,
'reason_code' => ExchangePowerShellEvidenceCaptureAdapter::OUTCOME_EMPTY_COLLECTION,
'summary_counts' => [
'total' => 0,
'processed' => 0,
'succeeded' => 0,
'failed' => 0,
'skipped' => 0,
'items' => 0,
],
])
->and($run->refresh()->summary_counts)->toBe([
'total' => 0,
'processed' => 0,
'succeeded' => 0,
'failed' => 0,
'skipped' => 0,
'items' => 0,
])
->and(TenantConfigurationResource::query()->count())->toBe(0)
->and(TenantConfigurationResourceEvidence::query()->count())->toBe(0);
});
it('Spec434 implementation has no Graph fallback path, product surface, tenant-id scope, or mini-platform', function (): void {
$runtimeFiles = [
app_path('Services/TenantConfiguration/ExchangePowerShellEvidenceCaptureAdapter.php'),
app_path('Services/TenantConfiguration/ExchangePowerShellCaptureEligibilityGate.php'),
app_path('Services/TenantConfiguration/ExchangePowerShellIdentityEvidenceGate.php'),
app_path('Services/TenantConfiguration/ExchangePowerShellContentOnlyEvidenceGuard.php'),
app_path('Services/TenantConfiguration/CoverageEvidenceWriter.php'),
];
$runtimeSource = collect($runtimeFiles)
->map(fn (string $file): string => file_get_contents($file) ?: '')
->implode("\n");
expect(preg_match('/\btenant_id\b/', $runtimeSource))->toBe(0)
->and($runtimeSource)
->not->toContain('ProviderGateway')
->not->toContain('GenericContentEvidenceCaptureService')
->not->toContain('listPolicies(')
->not->toContain('/deviceManagement')
->not->toContain('fake_empty_success')
->not->toContain('fake_captured')
->and(Schema::hasColumn('tenant_configuration_resources', 'tenant_id'))->toBeFalse()
->and(Schema::hasColumn('tenant_configuration_resource_evidence', 'tenant_id'))->toBeFalse()
->and(glob(app_path('Filament/**/*ExchangePowerShell*')) ?: [])->toBe([])
->and(glob(app_path('Livewire/**/*ExchangePowerShell*')) ?: [])->toBe([])
->and(glob(base_path('routes/*exchange*')) ?: [])->toBe([])
->and(glob(resource_path('views/**/*ExchangePowerShell*')) ?: [])->toBe([])
->and(glob(database_path('migrations/*exchange*evidence*')) ?: [])->toBe([])
->and(glob(database_path('migrations/*tenant*configuration*exchange*')) ?: [])->toBe([])
->and(File::exists(app_path('Jobs/TenantConfiguration/CaptureExchangePowerShellEvidenceJob.php')))->toBeFalse()
->and(File::exists(app_path('Console/Commands/CaptureExchangePowerShellEvidence.php')))->toBeFalse();
$captureOutcomeValues = array_map(
static fn (CaptureOutcome $outcome): string => $outcome->value,
CaptureOutcome::cases(),
);
expect($captureOutcomeValues)
->toContain(CaptureOutcome::Captured->value)
->not->toContain('fake_captured')
->not->toContain('fake_empty_success');
});
/**
* @param array<string, mixed> $overrides
*/
function spec434ReadyProviderConnection(
ManagedEnvironment $environment,
array $overrides = [],
bool $seedExchangePowerShellPermission = true,
): ProviderConnection {
$connection = ProviderConnection::factory()
->dedicated()
->consentGranted()
->create([
'workspace_id' => (int) $environment->workspace_id,
'managed_environment_id' => (int) $environment->getKey(),
'entra_tenant_id' => $environment->providerTenantContext(),
'is_default' => true,
'verification_status' => ProviderVerificationStatus::Healthy->value,
'last_health_check_at' => now(),
...$overrides,
]);
if ($seedExchangePowerShellPermission) {
spec434SeedExchangePowerShellPermission($environment, $connection);
}
return $connection;
}
/**
* @param array<string, mixed> $overrides
*/
function spec434SeedExchangePowerShellPermission(
ManagedEnvironment $environment,
ProviderConnection $connection,
array $overrides = [],
): ManagedEnvironmentPermission {
$details = [
'source' => 'provider_verification',
'observed_at' => now()->toJSON(),
'verified_at' => now()->toJSON(),
'evaluator' => 'exchange_powershell_permission_evidence',
'evaluator_version' => 'v1',
'workspace_id' => (int) $connection->workspace_id,
'managed_environment_id' => (int) $connection->managed_environment_id,
'provider' => (string) $connection->provider,
'provider_connection_id' => (int) $connection->getKey(),
];
if (is_array($overrides['details'] ?? null)) {
$details = array_replace($details, $overrides['details']);
}
unset($overrides['details']);
return ManagedEnvironmentPermission::query()->updateOrCreate(
[
'managed_environment_id' => (int) $environment->getKey(),
'permission_key' => 'Exchange.ManageAsApp',
'workspace_id' => (int) $environment->workspace_id,
],
[
'status' => 'granted',
'details' => $details,
'last_checked_at' => now(),
...$overrides,
],
);
}
/**
* @param array<string, mixed> $overrides
*/
function spec434Credential(ProviderConnection $connection, string $kind, array $overrides = []): ProviderCredential
{
return ProviderCredential::factory()->create([
'provider_connection_id' => (int) $connection->getKey(),
'type' => $kind,
'credential_kind' => $kind,
'source' => ProviderCredentialSource::DedicatedManual->value,
'last_rotated_at' => now(),
'expires_at' => now()->addYear(),
...$overrides,
]);
}
/**
* @param array<string, mixed> $overrides
* @param list<string> $resourceTypes
*/
function spec434CaptureRun(
ManagedEnvironment $environment,
User $user,
ProviderConnection $connection,
array $overrides = [],
array $resourceTypes = ['transportRule'],
): OperationRun {
$context = [
'target_scope' => [
'workspace_id' => (int) $environment->workspace_id,
'managed_environment_id' => (int) $environment->getKey(),
'provider_connection_id' => (int) $connection->getKey(),
],
'resource_types' => $resourceTypes,
'required_capability' => 'exchange_powershell_invoke',
];
if (is_array($overrides['context'] ?? null)) {
$context = array_replace_recursive($context, $overrides['context']);
}
unset($overrides['context']);
return OperationRun::factory()->withUser($user)->forTenant($environment)->create([
'type' => OperationRunType::TenantConfigurationCapture->value,
'status' => OperationRunStatus::Queued->value,
'outcome' => OperationRunOutcome::Pending->value,
'context' => $context,
...$overrides,
]);
}
/**
* @param array<string, mixed> $overrides
* @return array<string, mixed>
*/
function spec434TransportRulePayload(array $overrides = []): array
{
return [
'Guid' => '11111111-2222-3333-4444-555555555555',
'RuleId' => 'spec434-rule-1',
'DisplayName' => 'Spec 434 transport rule',
'Name' => 'Spec 434 transport rule',
'Priority' => 0,
'Mode' => 'Enforce',
'State' => 'Enabled',
'Enabled' => true,
'Conditions' => ['SenderDomainIs' => ['contoso.example']],
'Actions' => ['RedirectMessageTo' => ['security@contoso.example']],
'WhenChanged' => 'volatile',
...$overrides,
];
}
/**
* @return array<string, mixed>
*/
function spec434RemoteDomainPayload(): array
{
return [
'Guid' => '22222222-3333-4444-5555-666666666666',
'RemoteDomainId' => 'spec434-remote-domain-1',
'Identity' => 'spec434-remote-domain-1',
'DomainName' => 'contoso.example',
'DisplayName' => 'Contoso remote domain',
'IsDefault' => false,
'AllowedOOFType' => 'External',
'AutoReplyEnabled' => true,
];
}
/**
* @return array<string, mixed>
*/
function spec434InboundConnectorPayload(): array
{
return [
'Guid' => '33333333-4444-5555-6666-777777777777',
'ConnectorId' => 'spec434-inbound-connector-1',
'Identity' => 'spec434-inbound-connector-1',
'DisplayName' => 'Spec 434 inbound connector',
'Name' => 'Spec 434 inbound connector',
'ConnectorType' => 'Partner',
'Enabled' => true,
'RequireTls' => true,
];
}
/**
* @return array<string, mixed>
*/
function spec434PayloadFor(string $canonicalType): array
{
return match ($canonicalType) {
'transportRule' => spec434TransportRulePayload(),
'remoteDomain' => spec434RemoteDomainPayload(),
'inboundConnector' => spec434InboundConnectorPayload(),
default => throw new InvalidArgumentException('Unsupported Spec434 payload type.'),
};
}