206 lines
8.1 KiB
PHP
206 lines
8.1 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Models\OperationRun;
|
|
use App\Services\Operations\QueuedExecutionLegitimacyGate;
|
|
use App\Support\OperationRunOutcome;
|
|
use App\Support\OperationRunStatus;
|
|
use App\Support\Operations\ExecutionAuthorityMode;
|
|
use App\Support\Operations\ExecutionDenialClass;
|
|
use App\Support\Operations\ExecutionDenialReasonCode;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
|
|
uses(RefreshDatabase::class);
|
|
|
|
it('allows actor-bound inventory execution when the initiator still has capability', function (): void {
|
|
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
|
|
|
$run = OperationRun::factory()->create([
|
|
'tenant_id' => (int) $tenant->getKey(),
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'user_id' => (int) $user->getKey(),
|
|
'type' => 'inventory_sync',
|
|
'status' => OperationRunStatus::Queued->value,
|
|
'outcome' => OperationRunOutcome::Pending->value,
|
|
'context' => [
|
|
'execution_authority_mode' => ExecutionAuthorityMode::ActorBound->value,
|
|
],
|
|
]);
|
|
|
|
$decision = app(QueuedExecutionLegitimacyGate::class)->evaluate($run);
|
|
|
|
expect($decision->allowed)->toBeTrue()
|
|
->and($decision->authorityMode)->toBe(ExecutionAuthorityMode::ActorBound)
|
|
->and($decision->reasonCode)->toBeNull()
|
|
->and($decision->checks)->toMatchArray([
|
|
'workspace_scope' => 'passed',
|
|
'tenant_scope' => 'passed',
|
|
'capability' => 'passed',
|
|
'tenant_operability' => 'passed',
|
|
'execution_prerequisites' => 'not_applicable',
|
|
])
|
|
->and($decision->toArray())->toMatchArray([
|
|
'operation_type' => 'inventory_sync',
|
|
'authority_mode' => 'actor_bound',
|
|
'allowed' => true,
|
|
'retryable' => false,
|
|
'target_scope' => [
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'tenant_id' => (int) $tenant->getKey(),
|
|
'provider_connection_id' => null,
|
|
],
|
|
]);
|
|
});
|
|
|
|
it('denies actor-bound execution when the initiator loses capability', function (): void {
|
|
[$user, $tenant] = createUserWithTenant(role: 'readonly');
|
|
|
|
$run = OperationRun::factory()->create([
|
|
'tenant_id' => (int) $tenant->getKey(),
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'user_id' => (int) $user->getKey(),
|
|
'type' => 'inventory_sync',
|
|
'status' => OperationRunStatus::Queued->value,
|
|
'outcome' => OperationRunOutcome::Pending->value,
|
|
'context' => [
|
|
'execution_authority_mode' => ExecutionAuthorityMode::ActorBound->value,
|
|
],
|
|
]);
|
|
|
|
$decision = app(QueuedExecutionLegitimacyGate::class)->evaluate($run);
|
|
|
|
expect($decision->allowed)->toBeFalse()
|
|
->and($decision->denialClass)->toBe(ExecutionDenialClass::CapabilityDenied)
|
|
->and($decision->reasonCode)->toBe(ExecutionDenialReasonCode::MissingCapability)
|
|
->and($decision->retryable)->toBeFalse()
|
|
->and($decision->checks)->toMatchArray([
|
|
'workspace_scope' => 'passed',
|
|
'tenant_scope' => 'passed',
|
|
'capability' => 'failed',
|
|
]);
|
|
});
|
|
|
|
it('allows system-authority execution only for allowlisted operation types', function (): void {
|
|
[, $tenant] = createUserWithTenant(role: 'owner');
|
|
|
|
$run = OperationRun::factory()->create([
|
|
'tenant_id' => (int) $tenant->getKey(),
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'user_id' => null,
|
|
'type' => 'backup_schedule_run',
|
|
'status' => OperationRunStatus::Queued->value,
|
|
'outcome' => OperationRunOutcome::Pending->value,
|
|
'context' => [
|
|
'execution_authority_mode' => ExecutionAuthorityMode::SystemAuthority->value,
|
|
],
|
|
]);
|
|
|
|
$decision = app(QueuedExecutionLegitimacyGate::class)->evaluate($run);
|
|
|
|
expect($decision->allowed)->toBeTrue()
|
|
->and($decision->authorityMode)->toBe(ExecutionAuthorityMode::SystemAuthority)
|
|
->and($decision->initiator)->toMatchArray([
|
|
'identity_type' => 'system',
|
|
'user_id' => null,
|
|
]);
|
|
});
|
|
|
|
it('denies non-allowlisted system-authority execution with retryable prerequisite semantics', function (): void {
|
|
[, $tenant] = createUserWithTenant(role: 'owner');
|
|
|
|
$run = OperationRun::factory()->create([
|
|
'tenant_id' => (int) $tenant->getKey(),
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'user_id' => null,
|
|
'type' => 'inventory_sync',
|
|
'status' => OperationRunStatus::Queued->value,
|
|
'outcome' => OperationRunOutcome::Pending->value,
|
|
'context' => [
|
|
'execution_authority_mode' => ExecutionAuthorityMode::SystemAuthority->value,
|
|
],
|
|
]);
|
|
|
|
$decision = app(QueuedExecutionLegitimacyGate::class)->evaluate($run);
|
|
|
|
expect($decision->allowed)->toBeFalse()
|
|
->and($decision->denialClass)->toBe(ExecutionDenialClass::PrerequisiteInvalid)
|
|
->and($decision->reasonCode)->toBe(ExecutionDenialReasonCode::ExecutionPrerequisiteInvalid)
|
|
->and($decision->retryable)->toBeTrue()
|
|
->and($decision->checks['execution_prerequisites'])->toBe('failed');
|
|
});
|
|
|
|
it('maps write-gate blocks to retryable prerequisite denials', function (): void {
|
|
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
|
$tenant->forceFill([
|
|
'status' => 'archived',
|
|
'deleted_at' => now(),
|
|
'rbac_status' => 'not_configured',
|
|
'rbac_last_checked_at' => null,
|
|
])->save();
|
|
|
|
$run = OperationRun::factory()->create([
|
|
'tenant_id' => (int) $tenant->getKey(),
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'user_id' => (int) $user->getKey(),
|
|
'type' => 'restore.execute',
|
|
'status' => OperationRunStatus::Queued->value,
|
|
'outcome' => OperationRunOutcome::Pending->value,
|
|
'context' => [
|
|
'execution_authority_mode' => ExecutionAuthorityMode::ActorBound->value,
|
|
],
|
|
]);
|
|
|
|
$decision = app(QueuedExecutionLegitimacyGate::class)->evaluate($run);
|
|
|
|
expect($decision->allowed)->toBeFalse()
|
|
->and($decision->reasonCode)->toBe(ExecutionDenialReasonCode::WriteGateBlocked)
|
|
->and($decision->denialClass)->toBe(ExecutionDenialClass::PrerequisiteInvalid)
|
|
->and($decision->retryable)->toBeTrue()
|
|
->and($decision->checks['execution_prerequisites'])->toBe('failed');
|
|
});
|
|
|
|
it('infers tenant sync capability for policy sync runs from the central resolver', function (): void {
|
|
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
|
|
|
$run = OperationRun::factory()->create([
|
|
'tenant_id' => (int) $tenant->getKey(),
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'user_id' => (int) $user->getKey(),
|
|
'type' => 'policy.sync',
|
|
'status' => OperationRunStatus::Queued->value,
|
|
'outcome' => OperationRunOutcome::Pending->value,
|
|
'context' => [
|
|
'execution_authority_mode' => ExecutionAuthorityMode::ActorBound->value,
|
|
],
|
|
]);
|
|
|
|
$context = app(QueuedExecutionLegitimacyGate::class)->buildContext($run);
|
|
|
|
expect($context->requiredCapability)->toBe('tenant.sync')
|
|
->and($context->authorityMode)->toBe(ExecutionAuthorityMode::ActorBound);
|
|
});
|
|
|
|
it('infers system-authority schedule capability for backup schedule runs from the central resolver', function (): void {
|
|
[, $tenant] = createUserWithTenant(role: 'owner');
|
|
|
|
$run = OperationRun::factory()->create([
|
|
'tenant_id' => (int) $tenant->getKey(),
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'user_id' => null,
|
|
'type' => 'backup_schedule_run',
|
|
'status' => OperationRunStatus::Queued->value,
|
|
'outcome' => OperationRunOutcome::Pending->value,
|
|
'context' => [],
|
|
]);
|
|
|
|
$context = app(QueuedExecutionLegitimacyGate::class)->buildContext($run);
|
|
|
|
expect($context->requiredCapability)->toBe('tenant_backup_schedules.run')
|
|
->and($context->authorityMode)->toBe(ExecutionAuthorityMode::SystemAuthority)
|
|
->and($context->initiatorSnapshot())->toMatchArray([
|
|
'identity_type' => 'system',
|
|
'user_id' => null,
|
|
]);
|
|
});
|