544 lines
28 KiB
PHP
544 lines
28 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Models\ManagedEnvironment;
|
|
use App\Models\ManagedEnvironmentPermission;
|
|
use App\Models\ProviderConnection;
|
|
use App\Models\ProviderCredential;
|
|
use App\Models\TenantConfigurationResource;
|
|
use App\Models\TenantConfigurationResourceEvidence;
|
|
use App\Services\TenantConfiguration\ExchangePowerShellCommandContract;
|
|
use App\Services\TenantConfiguration\ExchangePowerShellCommandContracts;
|
|
use App\Services\TenantConfiguration\ExchangePowerShellCredentialReferenceResolver;
|
|
use App\Services\TenantConfiguration\ExchangePowerShellInvocationContext;
|
|
use App\Services\TenantConfiguration\ExchangePowerShellInvocationGate;
|
|
use App\Services\TenantConfiguration\ExchangePowerShellInvocationReadinessEvaluator;
|
|
use App\Services\TenantConfiguration\ExchangePowerShellPermissionEvidenceEvaluator;
|
|
use App\Services\TenantConfiguration\ExchangePowerShellProcessExecutor;
|
|
use App\Services\TenantConfiguration\ExchangePowerShellProcessResult;
|
|
use App\Services\TenantConfiguration\ExchangePowerShellProductionRunner;
|
|
use App\Services\TenantConfiguration\FakeExchangePowerShellProcessExecutor;
|
|
use App\Services\TenantConfiguration\ResourceTypeRegistry;
|
|
use App\Support\OperationRunOutcome;
|
|
use App\Support\OperationRunStatus;
|
|
use App\Support\Providers\Capabilities\ProviderCapabilityEvaluator;
|
|
use App\Support\Providers\Capabilities\ProviderCapabilityStatus;
|
|
use App\Support\Providers\ProviderConsentStatus;
|
|
use App\Support\Providers\ProviderCredentialKind;
|
|
use App\Support\Providers\ProviderCredentialSource;
|
|
use App\Support\Providers\ProviderVerificationStatus;
|
|
use Illuminate\Support\Facades\DB;
|
|
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' => [],
|
|
'tenantpilot.exchange_powershell.permission_evidence.currentness_days' => 30,
|
|
'tenantpilot.exchange_powershell.runtime.allowed_environments' => [app()->environment()],
|
|
'tenantpilot.exchange_powershell.runtime.module_check_enabled' => true,
|
|
]);
|
|
});
|
|
|
|
it('Spec433 uses existing provider credential and managed environment permission metadata without schema changes', function (): void {
|
|
[, $environment] = createMinimalUserWithTenant(role: 'owner');
|
|
$connection = spec433ProviderConnection($environment);
|
|
spec433Credential($connection, ProviderCredentialKind::Certificate->value);
|
|
spec433SeedExchangePowerShellPermission($environment, $connection);
|
|
|
|
config(['tenantpilot.exchange_powershell.credentials.supported_reference_kinds' => ['certificate']]);
|
|
|
|
$result = app(ExchangePowerShellInvocationReadinessEvaluator::class)->evaluate($environment, $connection);
|
|
|
|
expect($result->allowed)->toBeTrue()
|
|
->and($result->context['readiness_state'])->toBe('ready_for_live_invocation')
|
|
->and($result->context['credential_state'])->toBe('certificate_supported')
|
|
->and($result->context['permission_evidence_state'])->toBe('verified')
|
|
->and(Schema::hasColumn('provider_credentials', 'provider_connection_id'))->toBeTrue()
|
|
->and(Schema::hasColumn('provider_credentials', 'credential_kind'))->toBeTrue()
|
|
->and(Schema::hasColumn('provider_credentials', 'source'))->toBeTrue()
|
|
->and(Schema::hasColumn('provider_credentials', 'last_rotated_at'))->toBeTrue()
|
|
->and(Schema::hasColumn('provider_credentials', 'expires_at'))->toBeTrue()
|
|
->and(Schema::hasColumn('managed_environment_permissions', 'workspace_id'))->toBeTrue()
|
|
->and(Schema::hasColumn('managed_environment_permissions', 'details'))->toBeTrue()
|
|
->and(glob(database_path('migrations/*spec433*')) ?: [])->toBe([])
|
|
->and(glob(database_path('migrations/*exchange*credential*')) ?: [])->toBe([]);
|
|
});
|
|
|
|
it('Spec433 credential readiness blocks unsafe or unobservable states without leaking material', function (
|
|
?string $credentialKind,
|
|
array $overrides,
|
|
string $expectedFailureCode,
|
|
string $expectedState,
|
|
): void {
|
|
[, $environment] = createMinimalUserWithTenant(role: 'owner');
|
|
$connection = spec433ProviderConnection($environment, seedExchangePowerShellPermission: false);
|
|
|
|
if ($credentialKind !== null) {
|
|
spec433Credential($connection, $credentialKind, [
|
|
'payload' => [
|
|
'private_key' => 'spec433-private-key',
|
|
'certificate_password' => 'spec433-certificate-password',
|
|
'client_secret' => 'spec433-client-secret',
|
|
'access_token' => 'spec433-access-token',
|
|
],
|
|
...$overrides,
|
|
]);
|
|
}
|
|
|
|
$result = app(ExchangePowerShellCredentialReferenceResolver::class)->resolve($connection);
|
|
$payload = json_encode($result, JSON_THROW_ON_ERROR);
|
|
|
|
expect($result->allowed)->toBeFalse()
|
|
->and($result->failureCode)->toBe($expectedFailureCode)
|
|
->and($result->context['credential_state'])->toBe($expectedState)
|
|
->and($payload)
|
|
->not->toContain('spec433-private-key')
|
|
->not->toContain('spec433-certificate-password')
|
|
->not->toContain('spec433-client-secret')
|
|
->not->toContain('spec433-access-token')
|
|
->not->toContain('client_secret');
|
|
})->with([
|
|
'missing credential' => [null, [], 'credential_blocked_missing_reference', 'missing'],
|
|
'client secret' => [ProviderCredentialKind::ClientSecret->value, [], 'credential_blocked_secret_based_app', 'secret_based_app_unsupported'],
|
|
'certificate missing expiry metadata' => [ProviderCredentialKind::Certificate->value, ['expires_at' => null], 'credential_blocked_certificate_missing', 'certificate_missing'],
|
|
'certificate expired' => [ProviderCredentialKind::Certificate->value, ['expires_at' => now()->subDay()], 'credential_blocked_certificate_expired', 'certificate_expired'],
|
|
'certificate unsupported' => [ProviderCredentialKind::Certificate->value, [], 'credential_blocked_certificate_unsupported', 'certificate_unsupported'],
|
|
'federated unsupported' => [ProviderCredentialKind::Federated->value, [], 'credential_blocked_federated_unsupported', 'federated_unsupported'],
|
|
'managed identity unsupported' => ['managed_identity', [], 'credential_blocked_managed_identity_unsupported', 'managed_identity_unsupported'],
|
|
'unknown kind' => ['unknown_kind', [], 'credential_blocked_unknown_kind', 'unknown_kind'],
|
|
]);
|
|
|
|
it('Spec433 permission evidence enforces source currentness and provider connection scope', function (
|
|
array $detailsOverrides,
|
|
array $rowOverrides,
|
|
string $expectedFailureCode,
|
|
string $expectedState,
|
|
): void {
|
|
[, $environment] = createMinimalUserWithTenant(role: 'owner');
|
|
$connection = spec433ProviderConnection($environment, seedExchangePowerShellPermission: false);
|
|
|
|
spec433SeedExchangePowerShellPermission($environment, $connection, [
|
|
'details' => [
|
|
'source' => 'provider_verification',
|
|
'observed_at' => now()->toJSON(),
|
|
'verified_at' => now()->toJSON(),
|
|
'evaluator' => 'exchange_powershell_permission_evidence',
|
|
'evaluator_version' => 'v1',
|
|
...$detailsOverrides,
|
|
],
|
|
...$rowOverrides,
|
|
]);
|
|
|
|
$result = app(ExchangePowerShellPermissionEvidenceEvaluator::class)->evaluate($environment, $connection);
|
|
|
|
expect($result->allowed)->toBeFalse()
|
|
->and($result->failureCode)->toBe($expectedFailureCode)
|
|
->and($result->context['permission_evidence_state'])->toBe($expectedState);
|
|
})->with([
|
|
'absent' => [[], ['status' => 'missing'], 'permission_evidence_blocked_absent', 'absent'],
|
|
'unknown' => [[], ['status' => 'error'], 'permission_evidence_blocked_unknown', 'unknown'],
|
|
'source missing' => [['source' => null], [], 'permission_evidence_blocked_unvalidated', 'unvalidated'],
|
|
'source untrusted' => [['source' => 'manual_note'], [], 'permission_evidence_blocked_unvalidated', 'unvalidated'],
|
|
'source malformed' => [['source' => 'provider verification raw payload'], [], 'permission_evidence_blocked_unvalidated', 'unvalidated'],
|
|
'observed timestamp missing' => [['observed_at' => null], [], 'permission_evidence_blocked_unvalidated', 'unvalidated'],
|
|
'verified timestamp missing' => [['verified_at' => null], [], 'permission_evidence_blocked_unvalidated', 'unvalidated'],
|
|
'observed timestamp relative string' => [['observed_at' => 'now'], [], 'permission_evidence_blocked_unvalidated', 'unvalidated'],
|
|
'verified timestamp relative string' => [['verified_at' => 'now'], [], 'permission_evidence_blocked_unvalidated', 'unvalidated'],
|
|
'observed timestamp invalid calendar date' => [['observed_at' => '2026-02-30T10:00:00Z'], [], 'permission_evidence_blocked_unvalidated', 'unvalidated'],
|
|
'verified timestamp invalid offset' => [['verified_at' => '2026-07-07T22:41:10+24:00'], [], 'permission_evidence_blocked_unvalidated', 'unvalidated'],
|
|
'verified before observed' => [['observed_at' => now()->toJSON(), 'verified_at' => now()->subMinute()->toJSON()], [], 'permission_evidence_blocked_unvalidated', 'unvalidated'],
|
|
'evaluator missing' => [['evaluator' => null], [], 'permission_evidence_blocked_unvalidated', 'unvalidated'],
|
|
'evaluator version missing' => [['evaluator_version' => null], [], 'permission_evidence_blocked_unvalidated', 'unvalidated'],
|
|
'observed timestamp stale' => [['observed_at' => now()->subDays(31)->toJSON(), 'verified_at' => now()->toJSON()], [], 'permission_evidence_blocked_stale', 'stale'],
|
|
'verified timestamp stale' => [['observed_at' => now()->subDays(31)->subMinute()->toJSON(), 'verified_at' => now()->subDays(31)->toJSON()], [], 'permission_evidence_blocked_stale', 'stale'],
|
|
'stale by configurable threshold' => [[], ['last_checked_at' => now()->subDays(31)], 'permission_evidence_blocked_stale', 'stale'],
|
|
'wrong workspace' => [['workspace_id' => 999999], [], 'permission_evidence_blocked_wrong_workspace', 'wrong_workspace'],
|
|
'wrong environment' => [['managed_environment_id' => 999999], [], 'permission_evidence_blocked_wrong_environment', 'wrong_environment'],
|
|
'unsupported provider' => [['provider' => 'other'], [], 'permission_evidence_blocked_unsupported_provider', 'unsupported_provider'],
|
|
'wrong provider connection' => [['provider_connection_id' => 999999], [], 'permission_evidence_blocked_wrong_provider_connection', 'wrong_provider_connection'],
|
|
]);
|
|
|
|
it('Spec433 rejects malformed permission evidence scope identifiers before scope matching', function (
|
|
string $scopeKey,
|
|
string $malformedSuffix,
|
|
): void {
|
|
[, $environment] = createMinimalUserWithTenant(role: 'owner');
|
|
$connection = spec433ProviderConnection($environment, seedExchangePowerShellPermission: false);
|
|
$expectedValue = match ($scopeKey) {
|
|
'workspace_id' => (int) $connection->workspace_id,
|
|
'managed_environment_id' => (int) $connection->managed_environment_id,
|
|
'provider_connection_id' => (int) $connection->getKey(),
|
|
default => throw new InvalidArgumentException('Unsupported scope key for Spec433 test.'),
|
|
};
|
|
|
|
spec433SeedExchangePowerShellPermission($environment, $connection, [
|
|
'details' => [
|
|
$scopeKey => ((string) $expectedValue).$malformedSuffix,
|
|
],
|
|
]);
|
|
|
|
$result = app(ExchangePowerShellPermissionEvidenceEvaluator::class)->evaluate($environment, $connection);
|
|
|
|
expect($result->allowed)->toBeFalse()
|
|
->and($result->failureCode)->toBe('permission_evidence_blocked_unvalidated')
|
|
->and($result->context['permission_evidence_state'])->toBe('unvalidated');
|
|
})->with([
|
|
'workspace id numeric prefix' => fn (): array => [
|
|
'workspace_id',
|
|
'abc',
|
|
],
|
|
'managed environment id decimal' => fn (): array => [
|
|
'managed_environment_id',
|
|
'.0',
|
|
],
|
|
'provider connection id scientific notation' => fn (): array => [
|
|
'provider_connection_id',
|
|
'e0',
|
|
],
|
|
'provider connection id whitespace padded' => fn (): array => [
|
|
'provider_connection_id',
|
|
' ',
|
|
],
|
|
]);
|
|
|
|
it('Spec433 honors configured permission evidence currentness without fallback-to-latest behavior', function (): void {
|
|
config(['tenantpilot.exchange_powershell.permission_evidence.currentness_days' => 10]);
|
|
|
|
[, $environment] = createMinimalUserWithTenant(role: 'owner');
|
|
$connection = spec433ProviderConnection($environment, seedExchangePowerShellPermission: false);
|
|
spec433SeedExchangePowerShellPermission($environment, $connection, [
|
|
'last_checked_at' => now()->subDays(11),
|
|
]);
|
|
|
|
$result = app(ExchangePowerShellPermissionEvidenceEvaluator::class)->evaluate($environment, $connection);
|
|
|
|
expect($result->allowed)->toBeFalse()
|
|
->and($result->failureCode)->toBe('permission_evidence_blocked_stale')
|
|
->and($result->context['permission_currentness_days'])->toBe(10)
|
|
->and(ManagedEnvironmentPermission::query()
|
|
->where('managed_environment_id', (int) $environment->getKey())
|
|
->where('permission_key', 'Exchange.ManageAsApp')
|
|
->count())->toBe(1);
|
|
});
|
|
|
|
it('Spec433 combined readiness requires both credential and permission evidence and does not promote evidence', function (): void {
|
|
[, $environment] = createMinimalUserWithTenant(role: 'owner');
|
|
$connection = spec433ProviderConnection($environment, seedExchangePowerShellPermission: false);
|
|
spec433Credential($connection, ProviderCredentialKind::ClientSecret->value);
|
|
spec433SeedExchangePowerShellPermission($environment, $connection);
|
|
|
|
$secretCredentialResult = app(ExchangePowerShellInvocationReadinessEvaluator::class)->evaluate($environment, $connection);
|
|
|
|
$connection->credential()->delete();
|
|
config(['tenantpilot.exchange_powershell.credentials.supported_reference_kinds' => ['certificate']]);
|
|
spec433Credential($connection, ProviderCredentialKind::Certificate->value);
|
|
spec433SeedExchangePowerShellPermission($environment, $connection, [
|
|
'last_checked_at' => now()->subDays(31),
|
|
]);
|
|
|
|
$stalePermissionResult = app(ExchangePowerShellInvocationReadinessEvaluator::class)->evaluate($environment, $connection);
|
|
|
|
spec433SeedExchangePowerShellPermission($environment, $connection);
|
|
$readyResult = app(ExchangePowerShellInvocationReadinessEvaluator::class)->evaluate($environment, $connection);
|
|
|
|
expect($secretCredentialResult->allowed)->toBeFalse()
|
|
->and($secretCredentialResult->context['readiness_blocker'])->toBe('credential')
|
|
->and($stalePermissionResult->allowed)->toBeFalse()
|
|
->and($stalePermissionResult->context['readiness_blocker'])->toBe('permission')
|
|
->and($readyResult->allowed)->toBeTrue()
|
|
->and(TenantConfigurationResource::query()->count())->toBe(0)
|
|
->and(TenantConfigurationResourceEvidence::query()->count())->toBe(0);
|
|
});
|
|
|
|
it('Spec433 provider capability is supported only when combined readiness passes', function (
|
|
string $case,
|
|
array $connectionOverrides,
|
|
?string $credentialKind,
|
|
array $permissionOverrides,
|
|
?ProviderCapabilityStatus $expectedStatus,
|
|
): void {
|
|
config(['tenantpilot.exchange_powershell.credentials.supported_reference_kinds' => ['certificate']]);
|
|
|
|
[, $environment] = createMinimalUserWithTenant(role: 'owner');
|
|
$connection = spec433ProviderConnection($environment, $connectionOverrides, seedExchangePowerShellPermission: false);
|
|
|
|
if ($credentialKind !== null) {
|
|
spec433Credential($connection, $credentialKind);
|
|
}
|
|
|
|
if ($permissionOverrides !== ['missing' => true]) {
|
|
spec433SeedExchangePowerShellPermission($environment, $connection, $permissionOverrides);
|
|
}
|
|
|
|
$result = app(ProviderCapabilityEvaluator::class)->evaluate($environment, $connection, 'exchange_powershell_invoke');
|
|
|
|
expect($result->status)->toBe($expectedStatus)
|
|
->and($result->status)->not->toBe(
|
|
$case === 'ready' ? ProviderCapabilityStatus::Missing : ProviderCapabilityStatus::Supported,
|
|
);
|
|
})->with([
|
|
'admin consent missing' => ['admin_consent_missing', ['consent_status' => ProviderConsentStatus::Required->value, 'consent_granted_at' => null], ProviderCredentialKind::Certificate->value, [], ProviderCapabilityStatus::Missing],
|
|
'client secret' => ['client_secret', [], ProviderCredentialKind::ClientSecret->value, [], ProviderCapabilityStatus::Blocked],
|
|
'missing credential' => ['missing_credential', [], null, [], ProviderCapabilityStatus::Missing],
|
|
'missing permission' => ['missing_permission', [], ProviderCredentialKind::Certificate->value, ['missing' => true], ProviderCapabilityStatus::Missing],
|
|
'stale permission' => ['stale_permission', [], ProviderCredentialKind::Certificate->value, ['last_checked_at' => now()->subDays(31)], ProviderCapabilityStatus::Unknown],
|
|
'wrong scope permission' => ['wrong_scope_permission', [], ProviderCredentialKind::Certificate->value, ['details' => ['provider_connection_id' => 999999]], ProviderCapabilityStatus::Blocked],
|
|
'unsupported provider' => ['unsupported_provider', ['provider' => 'other'], ProviderCredentialKind::Certificate->value, ['details' => ['provider' => 'other']], ProviderCapabilityStatus::NotApplicable],
|
|
'ready' => ['ready', [], ProviderCredentialKind::Certificate->value, [], ProviderCapabilityStatus::Supported],
|
|
]);
|
|
|
|
it('Spec433 provider capability uses configured Exchange evidence currentness as the shared readiness truth', function (): void {
|
|
config([
|
|
'tenantpilot.exchange_powershell.credentials.supported_reference_kinds' => ['certificate'],
|
|
'tenantpilot.exchange_powershell.permission_evidence.currentness_days' => 45,
|
|
]);
|
|
|
|
[, $environment] = createMinimalUserWithTenant(role: 'owner');
|
|
$connection = spec433ProviderConnection($environment, seedExchangePowerShellPermission: false);
|
|
spec433Credential($connection, ProviderCredentialKind::Certificate->value);
|
|
spec433SeedExchangePowerShellPermission($environment, $connection, [
|
|
'details' => [
|
|
'observed_at' => now()->subDays(31)->toJSON(),
|
|
'verified_at' => now()->subDays(31)->toJSON(),
|
|
],
|
|
'last_checked_at' => now()->subDays(31),
|
|
]);
|
|
|
|
$capability = app(ProviderCapabilityEvaluator::class)->evaluate($environment, $connection, 'exchange_powershell_invoke');
|
|
$readiness = app(ExchangePowerShellInvocationReadinessEvaluator::class)->evaluate($environment, $connection);
|
|
|
|
expect($readiness->allowed)->toBeTrue()
|
|
->and($readiness->context['permission_currentness_days'])->toBe(45)
|
|
->and($capability->status)->toBe(ProviderCapabilityStatus::Supported);
|
|
});
|
|
|
|
it('Spec433 production runner consumes combined readiness before runtime or process execution', function (): void {
|
|
config(['tenantpilot.exchange_powershell.credentials.supported_reference_kinds' => ['certificate']]);
|
|
|
|
[, $environment] = createMinimalUserWithTenant(role: 'owner');
|
|
$connection = spec433ProviderConnection($environment, seedExchangePowerShellPermission: false);
|
|
spec433Credential($connection, ProviderCredentialKind::Certificate->value);
|
|
spec433SeedExchangePowerShellPermission($environment, $connection, [
|
|
'last_checked_at' => now()->subDays(31),
|
|
]);
|
|
$executor = spec433FakeExecutor();
|
|
|
|
$result = app(ExchangePowerShellProductionRunner::class)->run(
|
|
spec433VerifiedContract('transportRule'),
|
|
new ExchangePowerShellInvocationContext(
|
|
operationRunId: 433,
|
|
workspaceId: (int) $environment->workspace_id,
|
|
managedEnvironmentId: (int) $environment->getKey(),
|
|
providerConnectionId: (int) $connection->getKey(),
|
|
canonicalType: 'transportRule',
|
|
commandName: 'Get-TransportRule',
|
|
runnerMode: ExchangePowerShellInvocationGate::RUNNER_MODE_PRODUCTION,
|
|
redactionPolicy: ExchangePowerShellInvocationGate::REDACTION_POLICY,
|
|
sourceContractState: 'contract_verified_pending_capture',
|
|
),
|
|
);
|
|
|
|
expect($executor->calls)->toBe([])
|
|
->and($result->blocked)->toBeTrue()
|
|
->and($result->failureCode)->toBe('permission_evidence_blocked_stale')
|
|
->and($result->context['readiness_state'])->toBe('blocked')
|
|
->and($result->context['readiness_blocker'])->toBe('permission');
|
|
});
|
|
|
|
it('Spec433 invocation context persists only safe readiness states and no evidence rows after readiness pass', function (): void {
|
|
config(['tenantpilot.exchange_powershell.credentials.supported_reference_kinds' => ['certificate']]);
|
|
|
|
[$user, $environment] = createMinimalUserWithTenant(role: 'owner');
|
|
$connection = spec433ProviderConnection($environment);
|
|
spec433Credential($connection, ProviderCredentialKind::Certificate->value, [
|
|
'payload' => [
|
|
'private_key' => 'spec433-private-key',
|
|
'certificate_password' => 'spec433-certificate-password',
|
|
],
|
|
]);
|
|
spec433FakeExecutor([
|
|
ExchangePowerShellProcessResult::completed(0, "7.4.0\n"),
|
|
ExchangePowerShellProcessResult::completed(0, "ExchangeOnlineManagement\n"),
|
|
ExchangePowerShellProcessResult::completed(0, '[{"id":"rule-1","secret":"raw-output-secret"}]', durationMs: 14),
|
|
]);
|
|
|
|
$run = app(ExchangePowerShellInvocationGate::class)->invoke(
|
|
tenant: $environment,
|
|
providerConnection: $connection,
|
|
actor: $user,
|
|
canonicalType: 'transportRule',
|
|
runnerMode: ExchangePowerShellInvocationGate::RUNNER_MODE_PRODUCTION,
|
|
);
|
|
|
|
$persisted = json_encode([
|
|
'context' => $run->context,
|
|
'failure_summary' => $run->failure_summary,
|
|
'summary_counts' => $run->summary_counts,
|
|
], JSON_THROW_ON_ERROR);
|
|
|
|
expect($run->status)->toBe(OperationRunStatus::Completed->value)
|
|
->and($run->outcome)->toBe(OperationRunOutcome::Succeeded->value)
|
|
->and(data_get($run->context, 'runner_result.readiness_state'))->toBe('ready_for_live_invocation')
|
|
->and(data_get($run->context, 'runner_result.credential_state'))->toBe('certificate_supported')
|
|
->and(data_get($run->context, 'runner_result.permission_evidence_state'))->toBe('verified')
|
|
->and(TenantConfigurationResource::query()->count())->toBe(0)
|
|
->and(TenantConfigurationResourceEvidence::query()->count())->toBe(0)
|
|
->and($persisted)
|
|
->not->toContain('raw-output-secret')
|
|
->not->toContain('spec433-private-key')
|
|
->not->toContain('spec433-certificate-password')
|
|
->not->toContain('raw_shell_stdout')
|
|
->not->toContain('raw_shell_stderr')
|
|
->not->toContain('client_secret')
|
|
->not->toContain('access_token');
|
|
});
|
|
|
|
it('Spec433 adds no UI route trigger migration or tenant-id ownership surface', function (): void {
|
|
$runtimeFiles = [
|
|
app_path('Services/TenantConfiguration/ExchangePowerShellCredentialReferenceResolver.php'),
|
|
app_path('Services/TenantConfiguration/ExchangePowerShellPermissionEvidenceEvaluator.php'),
|
|
app_path('Services/TenantConfiguration/ExchangePowerShellInvocationReadinessEvaluator.php'),
|
|
app_path('Services/TenantConfiguration/ExchangePowerShellProductionRunner.php'),
|
|
app_path('Support/Providers/Capabilities/ProviderCapabilityEvaluator.php'),
|
|
];
|
|
$runtimeSource = collect($runtimeFiles)
|
|
->map(fn (string $file): string => file_get_contents($file) ?: '')
|
|
->implode("\n");
|
|
|
|
expect(preg_match('/\btenant_id\b/', $runtimeSource))->toBe(0);
|
|
|
|
expect($runtimeSource)
|
|
->not->toContain('Start-')
|
|
->not->toContain('Set-')
|
|
->not->toContain('New-')
|
|
->not->toContain('Remove-')
|
|
->and(glob(app_path('Filament/**/*ExchangePowerShell*')) ?: [])->toBe([])
|
|
->and(glob(base_path('routes/*exchange*')) ?: [])->toBe([])
|
|
->and(glob(resource_path('views/**/*ExchangePowerShell*')) ?: [])->toBe([])
|
|
->and(glob(database_path('migrations/*exchange*')) ?: [])->toBe([])
|
|
->and(File::exists(app_path('Jobs/TenantConfiguration/InvokeExchangePowerShell.php')))->toBeFalse();
|
|
});
|
|
|
|
/**
|
|
* @param array<string, mixed> $overrides
|
|
*/
|
|
function spec433ProviderConnection(
|
|
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) {
|
|
spec433SeedExchangePowerShellPermission($environment, $connection);
|
|
}
|
|
|
|
return $connection;
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $overrides
|
|
*/
|
|
function spec433SeedExchangePowerShellPermission(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 spec433Credential(ProviderConnection $connection, string $kind, array $overrides = []): ProviderCredential
|
|
{
|
|
$storedKind = in_array($kind, ProviderCredentialKind::values(), true)
|
|
? $kind
|
|
: ProviderCredentialKind::ClientSecret->value;
|
|
|
|
$credential = ProviderCredential::factory()->create([
|
|
'provider_connection_id' => (int) $connection->getKey(),
|
|
'type' => $storedKind,
|
|
'credential_kind' => $storedKind,
|
|
'source' => ProviderCredentialSource::DedicatedManual->value,
|
|
'last_rotated_at' => now(),
|
|
'expires_at' => now()->addYear(),
|
|
...$overrides,
|
|
]);
|
|
|
|
if ($storedKind !== $kind) {
|
|
DB::table('provider_credentials')
|
|
->where('id', (int) $credential->getKey())
|
|
->update([
|
|
'type' => $kind,
|
|
'credential_kind' => $kind,
|
|
]);
|
|
}
|
|
|
|
return $credential;
|
|
}
|
|
|
|
function spec433FakeExecutor(array $results = []): FakeExchangePowerShellProcessExecutor
|
|
{
|
|
$executor = new FakeExchangePowerShellProcessExecutor(results: $results);
|
|
app()->instance(ExchangePowerShellProcessExecutor::class, $executor);
|
|
|
|
return $executor;
|
|
}
|
|
|
|
function spec433VerifiedContract(string $canonicalType): ExchangePowerShellCommandContract
|
|
{
|
|
$contracts = new ExchangePowerShellCommandContracts;
|
|
$contract = $contracts->contractForCanonicalType($canonicalType);
|
|
|
|
if (! is_array($contract)) {
|
|
throw new RuntimeException('Missing Spec433 command contract.');
|
|
}
|
|
|
|
return ExchangePowerShellCommandContract::fromVerifiedArray($contract, $contracts, []);
|
|
}
|