Spec 430: Exchange PowerShell adapter contract slice 1. Validation was not rerun during PR creation handoff; branch contains implementation report and tests. Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #497
109 lines
6.7 KiB
PHP
109 lines
6.7 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Models\TenantConfigurationResourceType;
|
|
use App\Services\TenantConfiguration\CanonicalIdentityResolver;
|
|
use App\Services\TenantConfiguration\ClaimGuard;
|
|
use App\Services\TenantConfiguration\ExchangePowerShellCommandContracts;
|
|
use App\Services\TenantConfiguration\ResourceTypeRegistry;
|
|
use App\Support\TenantConfiguration\ClaimState;
|
|
use App\Support\TenantConfiguration\IdentityState;
|
|
|
|
it('Spec430 permission metadata remains pending runtime validation and does not widen provider readiness claims', function (string $canonicalType): void {
|
|
$contract = (new ExchangePowerShellCommandContracts)->contractForCanonicalType($canonicalType);
|
|
$permissions = $contract['permission_model'];
|
|
|
|
expect($permissions['status'])->toBe('runtime_validation_pending')
|
|
->and($permissions['known_permission_names'])->toBe(['Exchange.ManageAsApp'])
|
|
->and($permissions['least_privilege_runtime_validated'])->toBeFalse()
|
|
->and($permissions['exchange_rbac_runtime_validated'])->toBeFalse()
|
|
->and($permissions['admin_consent_required'])->toBeTrue()
|
|
->and($permissions['permission_failure_modes'])->toContain('permission_denied')
|
|
->and($permissions['permission_failure_modes'])->toContain('command_unavailable')
|
|
->and($permissions['permission_failure_modes'])->toContain('adapter_unavailable');
|
|
})->with(['transportRule', 'remoteDomain', 'inboundConnector']);
|
|
|
|
it('Spec430 response-shape metadata distinguishes empty denied unavailable malformed and unexpected shapes', function (string $canonicalType): void {
|
|
$shape = (new ExchangePowerShellCommandContracts)->contractForCanonicalType($canonicalType)['response_shape'];
|
|
|
|
expect($shape['raw_payload_shape'])->toBe('exchange_powershell_structured_collection')
|
|
->and($shape['collection_item_path'])->toBe('items')
|
|
->and($shape['empty_collection_meaning'])->toBe('valid_empty_collection')
|
|
->and($shape['permission_denied_response_meaning'])->toBe('permission_denied')
|
|
->and($shape['command_unavailable_response_meaning'])->toBe('command_unavailable')
|
|
->and($shape['adapter_unavailable_response_meaning'])->toBe('adapter_unavailable')
|
|
->and($shape['malformed_response_meaning'])->toBe('malformed_response')
|
|
->and($shape['unexpected_object_shape_meaning'])->toBe('unexpected_object_shape')
|
|
->and($shape['response_shape_safety'])->toBe('fail_closed_until_empty_denied_unavailable_malformed_and_unexpected_shapes_are_distinct');
|
|
})->with(['transportRule', 'remoteDomain', 'inboundConnector']);
|
|
|
|
it('Spec430 display-name-only payloads do not produce stable identity for included types', function (string $canonicalType): void {
|
|
$result = app(CanonicalIdentityResolver::class)->resolve(
|
|
spec430MetadataResourceType($canonicalType),
|
|
['DisplayName' => 'Display-only object', 'Name' => 'Display-only object'],
|
|
['source_contract_key' => 'exchange_powershell.'.$canonicalType, 'source_version' => ExchangePowerShellCommandContracts::COMMAND_CONTRACT_VERSION],
|
|
);
|
|
|
|
expect($result->identityState)->toBe(IdentityState::MissingExternalId)
|
|
->and($result->sourceResourceId)->toStartWith('missing:')
|
|
->and($result->canonicalResourceKey)->not->toContain('Display-only object');
|
|
})->with(['transportRule', 'remoteDomain', 'inboundConnector']);
|
|
|
|
it('Spec430 redaction metadata forbids secrets raw shell output content and protected configuration exposure', function (string $canonicalType): void {
|
|
$rules = (new ExchangePowerShellCommandContracts)->contractForCanonicalType($canonicalType)['redaction_rules'];
|
|
$json = json_encode($rules, JSON_THROW_ON_ERROR);
|
|
|
|
expect($rules['raw_payload_default_visible'])->toBeFalse()
|
|
->and($rules['permission_context_default_visible'])->toBeFalse()
|
|
->and($rules['forbidden_default_output'])->toContain('tokens')
|
|
->and($rules['forbidden_default_output'])->toContain('secrets')
|
|
->and($rules['forbidden_default_output'])->toContain('authorization_header')
|
|
->and($rules['forbidden_default_output'])->toContain('cookies')
|
|
->and($rules['forbidden_default_output'])->toContain('certificate_private_material')
|
|
->and($rules['forbidden_default_output'])->toContain('passwords')
|
|
->and($rules['forbidden_default_output'])->toContain('raw_transcripts')
|
|
->and($rules['forbidden_default_output'])->toContain('raw_shell_stdout')
|
|
->and($rules['forbidden_default_output'])->toContain('raw_shell_stderr')
|
|
->and($rules['forbidden_default_output'])->toContain('mail_body_content')
|
|
->and($rules['forbidden_default_output'])->toContain('mailbox_content')
|
|
->and($rules['forbidden_default_output'])->toContain('file_content')
|
|
->and($rules['forbidden_default_output'])->toContain('teams_transcript_content')
|
|
->and($json)->not->toContain('client-secret-value')
|
|
->and($json)->not->toContain('authorization-token-value');
|
|
})->with(['transportRule', 'remoteDomain', 'inboundConnector']);
|
|
|
|
it('Spec430 future execution scope metadata preserves workspace managed-environment and provider-connection handoff', function (string $canonicalType): void {
|
|
$scope = (new ExchangePowerShellCommandContracts)->contractForCanonicalType($canonicalType)['future_execution_scope'];
|
|
|
|
expect($scope)->toMatchArray([
|
|
'requires_workspace_id' => true,
|
|
'requires_managed_environment_id' => true,
|
|
'requires_provider_connection_id' => true,
|
|
'provider_native_scope_identifiers_are_metadata_only' => true,
|
|
]);
|
|
})->with(['transportRule', 'remoteDomain', 'inboundConnector']);
|
|
|
|
it('Spec430 claim guard allows only the exact internal adapter-contract statement', function (): void {
|
|
$guard = app(ClaimGuard::class);
|
|
|
|
expect($guard->evaluateStatement(ExchangePowerShellCommandContracts::ALLOWED_INTERNAL_CLAIM, internalOperatorOnly: true))
|
|
->toBe(ClaimState::InternalOnly)
|
|
->and($guard->evaluateStatement(ExchangePowerShellCommandContracts::ALLOWED_INTERNAL_CLAIM, internalOperatorOnly: false))
|
|
->toBe(ClaimState::ClaimBlocked)
|
|
->and($guard->evaluateStatement('Exchange PowerShell adapter contracts are customer ready', internalOperatorOnly: true))
|
|
->toBe(ClaimState::ClaimBlocked)
|
|
->and($guard->evaluateStatement('Exchange PowerShell coverage is certified', internalOperatorOnly: true))
|
|
->toBe(ClaimState::ClaimBlocked);
|
|
});
|
|
|
|
function spec430MetadataResourceType(string $canonicalType): TenantConfigurationResourceType
|
|
{
|
|
$definition = collect(ResourceTypeRegistry::defaultDefinitions())
|
|
->firstWhere('canonical_type', $canonicalType);
|
|
|
|
expect($definition)->not->toBeNull("Missing default resource type definition for {$canonicalType}.");
|
|
|
|
return new TenantConfigurationResourceType($definition);
|
|
}
|