Automated PR created by Codex via Gitea API. Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #459
480 lines
20 KiB
PHP
480 lines
20 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Support\ReviewPublicationResolution;
|
|
|
|
use App\Jobs\ScanEntraAdminRolesJob;
|
|
use App\Models\EnvironmentReview;
|
|
use App\Models\ManagedEnvironment;
|
|
use App\Models\OperationRun;
|
|
use App\Models\ReviewPublicationResolutionCase;
|
|
use App\Models\ReviewPublicationResolutionStep;
|
|
use App\Models\User;
|
|
use App\Services\EnvironmentReviews\EnvironmentReviewReadinessGate;
|
|
use App\Services\EnvironmentReviews\EnvironmentReviewService;
|
|
use App\Services\Evidence\EvidenceSnapshotService;
|
|
use App\Services\OperationRunService;
|
|
use App\Services\ReviewPackService;
|
|
use App\Services\Verification\StartVerification;
|
|
use App\Support\Audit\AuditActionId;
|
|
use App\Support\Auth\Capabilities;
|
|
use App\Support\OperationRunType;
|
|
use App\Support\ReviewPackStatus;
|
|
use Illuminate\Support\Facades\Gate;
|
|
use InvalidArgumentException;
|
|
use Throwable;
|
|
|
|
final class ReviewPublicationResolutionActionService
|
|
{
|
|
public function __construct(
|
|
private readonly ReviewPublicationResolutionService $caseService,
|
|
private readonly EvidenceSnapshotService $evidenceSnapshots,
|
|
private readonly EnvironmentReviewService $environmentReviews,
|
|
private readonly EnvironmentReviewReadinessGate $readinessGate,
|
|
private readonly ReviewPackService $reviewPacks,
|
|
private readonly OperationRunService $operationRuns,
|
|
private readonly StartVerification $verification,
|
|
private readonly ReviewPublicationResolutionStepAuthorizer $stepAuthorizer,
|
|
) {}
|
|
|
|
/**
|
|
* @return array{case:ReviewPublicationResolutionCase, step:ReviewPublicationResolutionStep, operation_run:?OperationRun, operation_type:?string}
|
|
*/
|
|
public function executeCurrentStep(ReviewPublicationResolutionCase $case, User $actor): array
|
|
{
|
|
Gate::forUser($actor)->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,
|
|
]);
|
|
|
|
if ($result->status === 'scope_busy') {
|
|
throw new InvalidArgumentException('Provider connection check is already running for another operation.');
|
|
}
|
|
|
|
$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(),
|
|
'review_publication_resolution_case_id' => (int) $case->getKey(),
|
|
'environment_review_id' => (int) $case->environment_review_id,
|
|
'trigger' => 'review_publication_resolution',
|
|
],
|
|
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);
|
|
}
|
|
}
|
|
}
|