## 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
233 lines
7.6 KiB
PHP
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(),
|
|
]);
|
|
});
|