Some checks failed
Main Confidence / confidence (push) Failing after 54s
## Summary - harden baseline capture truth, compare readiness, and monitoring explanations around latest inventory eligibility, blocked prerequisites, and zero-subject outcomes - improve onboarding verification and bootstrap recovery handling, including admin-consent callback invalidation and queued execution legitimacy/report behavior - align workspace findings/workspace overview signals and refresh the related spec, roadmap, and spec-candidate artifacts ## Validation - `cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent` - `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/BaselineDriftEngine/BaselineCaptureAuditEventsTest.php tests/Feature/BaselineDriftEngine/BaselineSnapshotNoTenantIdentifiersTest.php tests/Feature/BaselineDriftEngine/CaptureBaselineContentTest.php tests/Feature/BaselineDriftEngine/CaptureBaselineFullContentOnDemandTest.php tests/Feature/BaselineDriftEngine/CaptureBaselineMetaFallbackTest.php tests/Feature/Baselines/BaselineCaptureTest.php tests/Feature/Baselines/BaselineCompareFindingsTest.php tests/Feature/Baselines/BaselineSnapshotBackfillTest.php tests/Feature/Filament/BaselineCaptureResultExplanationSurfaceTest.php tests/Feature/Filament/BaselineCompareLandingStartSurfaceTest.php tests/Feature/Filament/BaselineProfileCaptureStartSurfaceTest.php tests/Feature/Filament/OperationRunBaselineTruthSurfaceTest.php tests/Feature/Monitoring/AuditCoverageGovernanceTest.php tests/Feature/Monitoring/GovernanceOperationRunSummariesTest.php tests/Feature/Notifications/OperationRunNotificationTest.php tests/Feature/Authorization/OperatorExplanationSurfaceAuthorizationTest.php` - `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/AdminConsentCallbackTest.php tests/Feature/Filament/WorkspaceOverviewDbOnlyTest.php tests/Feature/Guards/Spec194GovernanceActionSemanticsGuardTest.php tests/Feature/ManagedTenantOnboardingWizardTest.php tests/Feature/Onboarding/OnboardingVerificationTest.php tests/Feature/Operations/QueuedExecutionAuditTrailTest.php tests/Unit/Operations/QueuedExecutionLegitimacyGateTest.php` ## Notes - browser validation was not re-run in this pass Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #271
297 lines
12 KiB
PHP
297 lines
12 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Models\OperationRun;
|
|
use App\Models\ProviderConnection;
|
|
use App\Support\Auth\Capabilities;
|
|
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('allows onboarding verification runs for onboarding tenants when they originate from the onboarding wizard', function (): void {
|
|
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
|
|
|
$tenant->forceFill([
|
|
'status' => 'onboarding',
|
|
])->save();
|
|
|
|
$connection = ProviderConnection::factory()->create([
|
|
'tenant_id' => (int) $tenant->getKey(),
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'provider' => 'microsoft',
|
|
'entra_tenant_id' => (string) $tenant->tenant_id,
|
|
]);
|
|
|
|
$run = OperationRun::factory()->create([
|
|
'tenant_id' => (int) $tenant->getKey(),
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'user_id' => (int) $user->getKey(),
|
|
'type' => 'provider.connection.check',
|
|
'status' => OperationRunStatus::Queued->value,
|
|
'outcome' => OperationRunOutcome::Pending->value,
|
|
'context' => [
|
|
'execution_authority_mode' => ExecutionAuthorityMode::ActorBound->value,
|
|
'provider_connection_id' => (int) $connection->getKey(),
|
|
'wizard' => [
|
|
'flow' => 'managed_tenant_onboarding',
|
|
'step' => 'verification',
|
|
],
|
|
],
|
|
]);
|
|
|
|
$decision = app(QueuedExecutionLegitimacyGate::class)->evaluate($run);
|
|
|
|
expect($decision->allowed)->toBeTrue()
|
|
->and($decision->reasonCode)->toBeNull()
|
|
->and($decision->checks)->toMatchArray([
|
|
'workspace_scope' => 'passed',
|
|
'tenant_scope' => 'passed',
|
|
'capability' => 'passed',
|
|
'tenant_operability' => 'passed',
|
|
'execution_prerequisites' => 'passed',
|
|
]);
|
|
});
|
|
|
|
it('allows workspace-scoped onboarding bootstrap capabilities during queued reauthorization', function (): void {
|
|
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
|
|
|
$tenant->forceFill([
|
|
'status' => 'onboarding',
|
|
])->save();
|
|
|
|
$connection = ProviderConnection::factory()->create([
|
|
'tenant_id' => (int) $tenant->getKey(),
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'provider' => 'microsoft',
|
|
'entra_tenant_id' => (string) $tenant->tenant_id,
|
|
]);
|
|
|
|
$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,
|
|
'provider_connection_id' => (int) $connection->getKey(),
|
|
'required_capability' => Capabilities::WORKSPACE_MANAGED_TENANT_ONBOARD_BOOTSTRAP_INVENTORY_SYNC,
|
|
'wizard' => [
|
|
'flow' => 'managed_tenant_onboarding',
|
|
'step' => 'bootstrap',
|
|
],
|
|
],
|
|
]);
|
|
|
|
$decision = app(QueuedExecutionLegitimacyGate::class)->evaluate($run);
|
|
|
|
expect($decision->allowed)->toBeTrue()
|
|
->and($decision->reasonCode)->toBeNull()
|
|
->and($decision->checks)->toMatchArray([
|
|
'workspace_scope' => 'passed',
|
|
'tenant_scope' => 'passed',
|
|
'capability' => 'passed',
|
|
'tenant_operability' => 'passed',
|
|
'execution_prerequisites' => 'passed',
|
|
]);
|
|
});
|
|
|
|
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,
|
|
]);
|
|
});
|