TenantAtlas/tests/Feature/Operations/QueuedExecutionContractMatrixTest.php
ahmido 5bcb4f6ab8 feat: harden queued execution legitimacy (#179)
## Summary
- add a canonical queued execution legitimacy contract for actor-bound and system-authority operation runs
- enforce legitimacy before queued jobs transition runs to running across provider, inventory, restore, bulk, sync, and scheduled backup flows
- surface blocked execution outcomes consistently in Monitoring, notifications, audit data, and the tenantless operation viewer
- add Spec 149 artifacts and focused Pest coverage for legitimacy decisions, middleware ordering, blocked presentation, retry behavior, and cross-family adoption

## Testing
- vendor/bin/sail artisan test --compact tests/Unit/Operations/QueuedExecutionLegitimacyGateTest.php
- vendor/bin/sail artisan test --compact tests/Feature/Operations/QueuedExecutionMiddlewareOrderingTest.php
- vendor/bin/sail artisan test --compact tests/Feature/Verification/ProviderExecutionReauthorizationTest.php
- vendor/bin/sail artisan test --compact tests/Feature/Operations/RunInventorySyncExecutionReauthorizationTest.php
- vendor/bin/sail artisan test --compact tests/Feature/Operations/ExecuteRestoreRunExecutionReauthorizationTest.php
- vendor/bin/sail artisan test --compact tests/Feature/Operations/SystemRunBlockedExecutionNotificationTest.php
- vendor/bin/sail artisan test --compact tests/Feature/Operations/BulkOperationExecutionReauthorizationTest.php
- vendor/bin/sail artisan test --compact tests/Feature/Operations/QueuedExecutionRetryReauthorizationTest.php
- vendor/bin/sail artisan test --compact tests/Feature/Operations/QueuedExecutionContractMatrixTest.php
- vendor/bin/sail artisan test --compact tests/Feature/Operations/OperationRunBlockedExecutionPresentationTest.php
- vendor/bin/sail artisan test --compact tests/Feature/Operations/QueuedExecutionAuditTrailTest.php
- vendor/bin/sail artisan test --compact tests/Feature/Operations/TenantlessOperationRunViewerTest.php
- vendor/bin/sail bin pint --dirty --format agent

## Manual validation
- validated queued provider execution blocking for tenant operability drift in the integrated browser on /admin/operations and /admin/operations/{run}
- validated 404 vs 403 route behavior for non-membership vs in-scope capability denial
- validated initiator-null blocked system-run behavior without creating a user terminal notification

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #179
2026-03-17 21:52:40 +00:00

233 lines
7.6 KiB
PHP

<?php
declare(strict_types=1);
use App\Jobs\ProviderComplianceSnapshotJob;
use App\Jobs\ProviderInventorySyncJob;
use App\Jobs\RunBackupScheduleJob;
use App\Jobs\SyncPoliciesJob;
use App\Models\BackupSchedule;
use App\Models\OperationRun;
use App\Models\ProviderConnection;
use App\Models\Workspace;
use App\Services\Auth\CapabilityResolver;
use App\Services\OperationRunService;
use App\Services\Providers\ProviderOperationStartGate;
use App\Support\Auth\Capabilities;
function runQueuedContractMatrixJobThroughMiddleware(object $job, Closure $terminal): mixed
{
$pipeline = array_reduce(
array_reverse($job->middleware()),
fn (Closure $next, object $middleware): Closure => fn (object $job): mixed => $middleware->handle($job, $next),
$terminal,
);
return $pipeline($job);
}
it('blocks provider execution families consistently when provider capability is lost', function (string $operationType, string $jobClass): void {
[$user, $tenant] = createUserWithTenant(role: 'owner');
$connection = ProviderConnection::factory()->platform()->consentGranted()->create([
'tenant_id' => (int) $tenant->getKey(),
'provider' => 'microsoft',
'entra_tenant_id' => fake()->uuid(),
'status' => 'connected',
]);
$run = app(OperationRunService::class)->ensureRun(
tenant: $tenant,
type: $operationType,
inputs: [
'provider_connection_id' => (int) $connection->getKey(),
'required_capability' => Capabilities::PROVIDER_RUN,
],
initiator: $user,
);
$job = new $jobClass(
tenantId: (int) $tenant->getKey(),
userId: (int) $user->getKey(),
providerConnectionId: (int) $connection->getKey(),
operationRun: $run,
);
$user->tenantMemberships()->where('tenant_id', $tenant->getKey())->update(['role' => 'readonly']);
app(CapabilityResolver::class)->clearCache();
$terminalInvoked = false;
runQueuedContractMatrixJobThroughMiddleware(
$job,
function () use (&$terminalInvoked): string {
$terminalInvoked = true;
return 'ran';
},
);
$run->refresh();
expect($terminalInvoked)->toBeFalse()
->and($run->outcome)->toBe('blocked')
->and($run->context['reason_code'] ?? null)->toBe('missing_capability')
->and($run->context['execution_legitimacy']['metadata']['required_capability'] ?? null)->toBe(Capabilities::PROVIDER_RUN);
})->with([
'provider inventory sync' => ['inventory_sync', ProviderInventorySyncJob::class],
'provider compliance snapshot' => ['compliance.snapshot', ProviderComplianceSnapshotJob::class],
]);
it('blocks policy sync before side effects when sync capability is lost', function (): void {
[$user, $tenant] = createUserWithTenant(role: 'owner');
$run = app(OperationRunService::class)->ensureRun(
tenant: $tenant,
type: 'policy.sync',
inputs: [
'scope' => 'all',
'types' => ['deviceConfiguration'],
],
initiator: $user,
);
$job = new SyncPoliciesJob(
tenantId: (int) $tenant->getKey(),
types: ['deviceConfiguration'],
policyIds: null,
operationRun: $run,
);
$user->tenantMemberships()->where('tenant_id', $tenant->getKey())->update(['role' => 'readonly']);
app(CapabilityResolver::class)->clearCache();
$terminalInvoked = false;
runQueuedContractMatrixJobThroughMiddleware(
$job,
function () use (&$terminalInvoked): string {
$terminalInvoked = true;
return 'ran';
},
);
$run->refresh();
expect($terminalInvoked)->toBeFalse()
->and($run->outcome)->toBe('blocked')
->and($run->context['reason_code'] ?? null)->toBe('missing_capability')
->and($run->context['execution_legitimacy']['metadata']['required_capability'] ?? null)->toBe(Capabilities::TENANT_SYNC);
});
it('blocks scheduled backup runs under system authority when tenant operability drifts', function (): void {
[, $tenant] = createUserWithTenant(role: 'owner');
$schedule = BackupSchedule::query()->create([
'tenant_id' => (int) $tenant->getKey(),
'name' => 'Queued legitimacy contract',
'is_enabled' => true,
'timezone' => 'UTC',
'frequency' => 'daily',
'time_of_day' => '01:00:00',
'days_of_week' => null,
'policy_types' => ['deviceConfiguration'],
'include_foundations' => true,
'retention_keep_last' => 30,
'next_run_at' => null,
]);
$run = app(OperationRunService::class)->ensureRunWithIdentityStrict(
tenant: $tenant,
type: 'backup_schedule_run',
identityInputs: [
'backup_schedule_id' => (int) $schedule->getKey(),
'scheduled_for' => now()->toDateTimeString(),
],
context: [
'backup_schedule_id' => (int) $schedule->getKey(),
'trigger' => 'scheduled',
],
);
$otherWorkspace = Workspace::factory()->create();
$tenant->forceFill([
'workspace_id' => (int) $otherWorkspace->getKey(),
])->save();
$job = new RunBackupScheduleJob(operationRun: $run, backupScheduleId: (int) $schedule->getKey());
$terminalInvoked = false;
runQueuedContractMatrixJobThroughMiddleware(
$job,
function () use (&$terminalInvoked): string {
$terminalInvoked = true;
return 'ran';
},
);
$run->refresh();
expect($terminalInvoked)->toBeFalse()
->and($run->outcome)->toBe('blocked')
->and($run->context['reason_code'] ?? null)->toBe('workspace_mismatch')
->and($run->context['execution_legitimacy']['authority_mode'] ?? null)->toBe('system_authority');
});
it('stores canonical execution metadata in existing operation run context structures', function (): void {
[$user, $tenant] = createUserWithTenant(role: 'owner');
$policyRun = app(OperationRunService::class)->ensureRun(
tenant: $tenant,
type: 'policy.sync',
inputs: [
'scope' => 'all',
'types' => ['deviceConfiguration'],
],
initiator: $user,
);
$scheduleRun = app(OperationRunService::class)->ensureRunWithIdentityStrict(
tenant: $tenant,
type: 'backup_schedule_run',
identityInputs: [
'backup_schedule_id' => 99,
'scheduled_for' => now()->toDateTimeString(),
],
context: [
'backup_schedule_id' => 99,
'trigger' => 'scheduled',
],
);
$connection = ProviderConnection::factory()->platform()->consentGranted()->create([
'tenant_id' => (int) $tenant->getKey(),
'provider' => 'microsoft',
'entra_tenant_id' => fake()->uuid(),
'status' => 'connected',
]);
$result = app(ProviderOperationStartGate::class)->start(
tenant: $tenant,
connection: $connection,
operationType: 'inventory_sync',
dispatcher: static fn (OperationRun $run): null => null,
initiator: $user,
);
expect($policyRun->context)->toMatchArray([
'execution_authority_mode' => 'actor_bound',
'required_capability' => Capabilities::TENANT_SYNC,
])
->and($scheduleRun->context)->toMatchArray([
'execution_authority_mode' => 'system_authority',
'required_capability' => Capabilities::TENANT_BACKUP_SCHEDULES_RUN,
])
->and($result->run->context)->toMatchArray([
'execution_authority_mode' => 'actor_bound',
'required_capability' => Capabilities::PROVIDER_RUN,
'provider_connection_id' => (int) $connection->getKey(),
]);
});