authorize('executeStep', $case); $case = $this->caseService->refreshCase($case, $actor); Gate::forUser($actor)->authorize('executeStep', $case); $step = $case->currentStep(); if (! $step instanceof ReviewPublicationResolutionStep) { throw new InvalidArgumentException('There is no actionable resolution step.'); } $stepKey = ReviewPublicationResolutionStepKey::tryFrom((string) $step->step_key); if (! $stepKey instanceof ReviewPublicationResolutionStepKey) { throw new InvalidArgumentException('The current resolution step is not recognized.'); } $case->loadMissing(['environmentReview.tenant', 'tenant']); $review = $case->environmentReview; $tenant = $case->tenant; if (! $review instanceof EnvironmentReview || ! $tenant instanceof ManagedEnvironment) { throw new InvalidArgumentException('The resolution case is missing its review context.'); } if (! $this->stepAuthorizer->canExecuteStep($actor, $tenant, $step)) { abort(403); } $this->caseService->recordAudit( case: $case, action: AuditActionId::ReviewPublicationResolutionStepStarted, actor: $actor, metadata: [ 'step_key' => $stepKey->value, ], ); try { return match ($stepKey) { ReviewPublicationResolutionStepKey::CompleteRequiredReports => $this->completeRequiredReports($case, $step, $tenant, $actor), ReviewPublicationResolutionStepKey::CollectEvidenceSnapshot => $this->collectEvidence($case, $step, $tenant, $actor), ReviewPublicationResolutionStepKey::RefreshReviewComposition => $this->refreshReviewComposition($case, $step, $review, $actor), ReviewPublicationResolutionStepKey::GenerateReviewPack => $this->generateReviewPack($case, $step, $review, $actor), ReviewPublicationResolutionStepKey::ReturnToPublication => $this->returnToPublication($case, $step, $actor), ReviewPublicationResolutionStepKey::ValidateReviewReadiness => throw new InvalidArgumentException('Readiness validation is automatic and cannot be manually executed.'), }; } catch (Throwable $throwable) { $step->forceFill([ 'status' => ReviewPublicationResolutionStepStatus::Failed->value, 'failed_at' => now(), 'summary' => array_replace(is_array($step->summary) ? $step->summary : [], [ 'state_description' => 'Step failed before it could be queued.', 'failure_code' => 'review_publication_resolution.step_failed_before_queue', ]), ])->save(); $case->forceFill([ 'status' => ReviewPublicationResolutionCaseStatus::Blocked->value, 'current_step_key' => $stepKey->value, ])->save(); $this->caseService->recordAudit( case: $case, action: AuditActionId::ReviewPublicationResolutionStepFailed, actor: $actor, metadata: [ 'step_key' => $stepKey->value, 'failure_code' => 'review_publication_resolution.step_failed_before_queue', ], ); throw $throwable; } } /** * @return array{case:ReviewPublicationResolutionCase, step:ReviewPublicationResolutionStep, operation_run:?OperationRun, operation_type:?string} */ private function completeRequiredReports( ReviewPublicationResolutionCase $case, ReviewPublicationResolutionStep $step, ManagedEnvironment $tenant, User $actor, ): array { $missingDimensions = array_values(array_filter( (array) data_get($step->summary, 'missing_report_dimensions', []), static fn (mixed $dimension): bool => is_string($dimension) && trim($dimension) !== '', )); $targetDimension = (string) ($missingDimensions[0] ?? ''); if ($targetDimension === '') { $step->forceFill([ 'status' => ReviewPublicationResolutionStepStatus::Completed->value, 'completed_at' => now(), 'summary' => array_replace(is_array($step->summary) ? $step->summary : [], [ 'state_description' => 'Required reports are already current.', ]), ])->save(); $this->caseService->refreshCase($case, $actor); return [ 'case' => $case->fresh(['steps.operationRun', 'environmentReview.tenant']), 'step' => $step->fresh('operationRun'), 'operation_run' => null, 'operation_type' => null, ]; } if ($targetDimension === 'permission_posture') { Gate::forUser($actor)->authorize(Capabilities::PROVIDER_RUN, $tenant); $result = $this->verification->providerConnectionCheckForTenant($tenant, $actor, [ 'trigger' => 'review_publication_resolution', 'review_publication_resolution_case_id' => (int) $case->getKey(), 'environment_review_id' => (int) $case->environment_review_id, ]); $this->markQueuedOrCompleted( case: $case, step: $step, proofType: 'operation_run', proofId: (int) $result->run->getKey(), proofStatus: (string) $result->run->status, operationRun: $result->run, actor: $actor, operationType: 'provider.connection.check', summary: [ 'target_report_dimension' => $targetDimension, ], ); return [ 'case' => $case->fresh(['steps.operationRun', 'environmentReview.tenant']), 'step' => $step->fresh('operationRun'), 'operation_run' => $result->run, 'operation_type' => 'provider.connection.check', ]; } if ($targetDimension === 'entra_admin_roles') { Gate::forUser($actor)->authorize(Capabilities::ENTRA_ROLES_MANAGE, $tenant); $operationRun = $this->operationRuns->ensureRunWithIdentity( tenant: $tenant, type: OperationRunType::EntraAdminRolesScan->value, identityInputs: [ 'managed_environment_id' => (int) $tenant->getKey(), 'trigger' => 'scan', ], context: [ 'workspace_id' => (int) $tenant->workspace_id, 'initiator_user_id' => (int) $actor->getKey(), 'review_publication_resolution_case_id' => (int) $case->getKey(), 'environment_review_id' => (int) $case->environment_review_id, 'trigger' => 'review_publication_resolution', ], initiator: $actor, ); if ($operationRun->wasRecentlyCreated) { $this->operationRuns->dispatchOrFail( $operationRun, fn (): mixed => ScanEntraAdminRolesJob::dispatch( tenantId: (int) $tenant->getKey(), workspaceId: (int) $tenant->workspace_id, initiatorUserId: (int) $actor->getKey(), ), ); } $this->markQueuedOrCompleted( case: $case, step: $step, proofType: 'operation_run', proofId: (int) $operationRun->getKey(), proofStatus: (string) $operationRun->status, operationRun: $operationRun, actor: $actor, operationType: OperationRunType::EntraAdminRolesScan->value, summary: [ 'target_report_dimension' => $targetDimension, ], ); return [ 'case' => $case->fresh(['steps.operationRun', 'environmentReview.tenant']), 'step' => $step->fresh('operationRun'), 'operation_run' => $operationRun, 'operation_type' => OperationRunType::EntraAdminRolesScan->value, ]; } throw new InvalidArgumentException('The required report dimension is not executable by this workflow.'); } /** * @return array{case:ReviewPublicationResolutionCase, step:ReviewPublicationResolutionStep, operation_run:?OperationRun, operation_type:?string} */ private function collectEvidence( ReviewPublicationResolutionCase $case, ReviewPublicationResolutionStep $step, ManagedEnvironment $tenant, User $actor, ): array { Gate::forUser($actor)->authorize(Capabilities::EVIDENCE_MANAGE, $tenant); $snapshot = $this->evidenceSnapshots->generate($tenant, $actor); $operationRun = $snapshot->operationRun; $this->markQueuedOrCompleted( case: $case, step: $step, proofType: 'evidence_snapshot', proofId: (int) $snapshot->getKey(), proofStatus: (string) $snapshot->status, operationRun: $operationRun, actor: $actor, operationType: OperationRunType::EvidenceSnapshotGenerate->value, ); return [ 'case' => $case->fresh(['steps.operationRun', 'environmentReview.tenant']), 'step' => $step->fresh('operationRun'), 'operation_run' => $operationRun, 'operation_type' => OperationRunType::EvidenceSnapshotGenerate->value, ]; } /** * @return array{case:ReviewPublicationResolutionCase, step:ReviewPublicationResolutionStep, operation_run:?OperationRun, operation_type:?string} */ private function refreshReviewComposition( ReviewPublicationResolutionCase $case, ReviewPublicationResolutionStep $step, EnvironmentReview $review, User $actor, ): array { Gate::forUser($actor)->authorize('refresh', $review); $review = $this->environmentReviews->refresh($review, $actor); $operationRun = $review->operationRun; $this->markQueuedOrCompleted( case: $case, step: $step, proofType: 'environment_review', proofId: (int) $review->getKey(), proofStatus: (string) $review->status, operationRun: $operationRun, actor: $actor, operationType: OperationRunType::EnvironmentReviewCompose->value, ); return [ 'case' => $case->fresh(['steps.operationRun', 'environmentReview.tenant']), 'step' => $step->fresh('operationRun'), 'operation_run' => $operationRun, 'operation_type' => OperationRunType::EnvironmentReviewCompose->value, ]; } /** * @return array{case:ReviewPublicationResolutionCase, step:ReviewPublicationResolutionStep, operation_run:?OperationRun, operation_type:?string} */ private function generateReviewPack( ReviewPublicationResolutionCase $case, ReviewPublicationResolutionStep $step, EnvironmentReview $review, User $actor, ): array { Gate::forUser($actor)->authorize('export', $review); if (! $this->readinessGate->canExport($review)) { throw new InvalidArgumentException('Review blockers must be resolved before generating the publication pack.'); } $pack = $this->reviewPacks->generateFromReview($review, $actor, [ 'include_pii' => false, 'include_operations' => true, ]); $operationRun = $pack->operationRun; if ($pack->status === ReviewPackStatus::Ready->value && (int) $review->current_export_review_pack_id !== (int) $pack->getKey()) { $review->forceFill(['current_export_review_pack_id' => (int) $pack->getKey()])->save(); } $this->markQueuedOrCompleted( case: $case, step: $step, proofType: 'review_pack', proofId: (int) $pack->getKey(), proofStatus: (string) $pack->status, operationRun: $operationRun, actor: $actor, operationType: OperationRunType::ReviewPackGenerate->value, ); return [ 'case' => $case->fresh(['steps.operationRun', 'environmentReview.tenant']), 'step' => $step->fresh('operationRun'), 'operation_run' => $operationRun, 'operation_type' => OperationRunType::ReviewPackGenerate->value, ]; } /** * @return array{case:ReviewPublicationResolutionCase, step:ReviewPublicationResolutionStep, operation_run:?OperationRun, operation_type:?string} */ private function returnToPublication( ReviewPublicationResolutionCase $case, ReviewPublicationResolutionStep $step, User $actor, ): array { $step->forceFill([ 'status' => ReviewPublicationResolutionStepStatus::Completed->value, 'completed_at' => now(), 'proof_type' => 'environment_review', 'proof_id' => (int) $case->environment_review_id, 'proof_status' => 'ready', 'summary' => array_replace(is_array($step->summary) ? $step->summary : [], [ 'state_description' => 'Returned to the publication workflow.', ]), ])->save(); $case->forceFill([ 'status' => ReviewPublicationResolutionCaseStatus::Completed->value, 'current_step_key' => null, 'completed_at' => now(), ])->save(); $this->caseService->recordAudit( case: $case, action: AuditActionId::ReviewPublicationResolutionStepCompleted, actor: $actor, metadata: [ 'step_key' => ReviewPublicationResolutionStepKey::ReturnToPublication->value, ], ); $this->caseService->recordAudit( case: $case, action: AuditActionId::ReviewPublicationResolutionCompleted, actor: $actor, metadata: [ 'environment_review_id' => (int) $case->environment_review_id, ], ); return [ 'case' => $case->fresh(['steps.operationRun', 'environmentReview.tenant']), 'step' => $step->fresh('operationRun'), 'operation_run' => null, 'operation_type' => null, ]; } private function markQueuedOrCompleted( ReviewPublicationResolutionCase $case, ReviewPublicationResolutionStep $step, string $proofType, int $proofId, string $proofStatus, ?OperationRun $operationRun, User $actor, string $operationType, array $summary = [], ): void { $isReadyProof = in_array($proofStatus, ['active', 'ready', 'complete'], true); $step->forceFill([ 'status' => $isReadyProof && ! $operationRun?->wasRecentlyCreated ? ReviewPublicationResolutionStepStatus::Completed->value : ReviewPublicationResolutionStepStatus::Running->value, 'started_at' => $step->started_at ?? now(), 'completed_at' => $isReadyProof && ! $operationRun?->wasRecentlyCreated ? now() : $step->completed_at, 'operation_run_id' => $operationRun instanceof OperationRun ? (int) $operationRun->getKey() : null, 'proof_type' => $proofType, 'proof_id' => $proofId, 'proof_status' => $proofStatus, 'summary' => array_replace(is_array($step->summary) ? $step->summary : [], [ 'state_description' => $isReadyProof && ! $operationRun?->wasRecentlyCreated ? 'Requirement is satisfied.' : 'Queued for execution. Open the linked operation for progress.', ], $summary), ])->save(); $case->forceFill([ 'status' => $step->status === ReviewPublicationResolutionStepStatus::Running->value ? ReviewPublicationResolutionCaseStatus::WaitingForRun->value : ReviewPublicationResolutionCaseStatus::InProgress->value, 'current_step_key' => (string) $step->step_key, ])->save(); if ($operationRun instanceof OperationRun) { $this->caseService->recordAudit( case: $case, action: AuditActionId::ReviewPublicationResolutionOperationLinked, actor: $actor, metadata: [ 'step_key' => (string) $step->step_key, 'operation_type' => $operationType, 'proof_type' => $proofType, 'proof_id' => $proofId, ], operationRun: $operationRun, ); } if ($step->status === ReviewPublicationResolutionStepStatus::Completed->value) { $this->caseService->recordAudit( case: $case, action: AuditActionId::ReviewPublicationResolutionStepCompleted, actor: $actor, metadata: [ 'step_key' => (string) $step->step_key, 'proof_type' => $proofType, 'proof_id' => $proofId, ], operationRun: $operationRun, ); $this->caseService->refreshCase($case, $actor); } } }