feat: guard Project status transitions
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service\Project;
|
||||
|
||||
use App\Entity\Project\Project;
|
||||
use App\Entity\Project\ProjectStatus;
|
||||
use App\Entity\User\User;
|
||||
use App\Security\Voter\UserOwnedVoterTrait;
|
||||
|
||||
class ProjectService
|
||||
{
|
||||
use UserOwnedVoterTrait;
|
||||
|
||||
public const OWNER_VALID_TRANSITIONS = [
|
||||
ProjectStatus::InDraft->value => [ProjectStatus::ToCampaignReview],
|
||||
ProjectStatus::InCampaignReviewToChange->value => [ProjectStatus::InCampaignReviewToReview],
|
||||
ProjectStatus::ToCampaign->value => [ProjectStatus::InCampaign],
|
||||
ProjectStatus::InFundingReviewToChange->value => [ProjectStatus::InFundingReviewToReview],
|
||||
ProjectStatus::ToFunding->value => [ProjectStatus::InFunding],
|
||||
];
|
||||
|
||||
/**
|
||||
* Check if an User can move a Project to some status.
|
||||
*
|
||||
* @param User $actor The User that is to apply the status change
|
||||
* @param Project $project The Project in their current (pre-change) state
|
||||
* @param ProjectStatus $to The ProjectStatus that the actor wishes to move the Project to
|
||||
*/
|
||||
public function canTransition(User $actor, Project $project, ProjectStatus $to): bool
|
||||
{
|
||||
if ($actor->hasRoles(['ROLE_ADMIN'])) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!$this->isOwnerOf($project, $actor)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$from = $project->getStatus();
|
||||
|
||||
if (
|
||||
array_key_exists($from->value, self::OWNER_VALID_TRANSITIONS)
|
||||
&& in_array($to, self::OWNER_VALID_TRANSITIONS[$from->value])
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\State\Project;
|
||||
|
||||
use ApiPlatform\Metadata\Exception\AccessDeniedException;
|
||||
use ApiPlatform\Metadata\Operation;
|
||||
use ApiPlatform\State\ProcessorInterface;
|
||||
use App\ApiResource\Project\ProjectApiResource;
|
||||
@@ -10,6 +11,7 @@ use App\Dto\ProjectUpdationDto;
|
||||
use App\Entity\Project\Project;
|
||||
use App\Entity\Project\ProjectCalendar;
|
||||
use App\Mapping\AutoMapper;
|
||||
use App\Service\Project\ProjectService;
|
||||
use App\State\EntityStateProcessor;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
use Symfony\Component\Security\Core\Exception\AuthenticationException;
|
||||
@@ -20,22 +22,20 @@ class ProjectStateProcessor implements ProcessorInterface
|
||||
private EntityStateProcessor $entityStateProcessor,
|
||||
private AutoMapper $autoMapper,
|
||||
private Security $security,
|
||||
private ProjectService $projectService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @param ProjectCreationDto|ProjectUpdationDto $data
|
||||
* @param array{id: int} $uriVariables
|
||||
*
|
||||
* @return ProjectApiResource|null
|
||||
*/
|
||||
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = [])
|
||||
{
|
||||
if ($data instanceof ProjectCreationDto) {
|
||||
$project = $this->getProjectFromCreation($data);
|
||||
}
|
||||
|
||||
if ($data instanceof ProjectUpdationDto) {
|
||||
$project = $this->getProjectFromUpdate($data, $uriVariables);
|
||||
} elseif ($data instanceof ProjectUpdationDto) {
|
||||
$project = $this->getProjectFromUpdate($data, $context);
|
||||
} else {
|
||||
throw new \InvalidArgumentException('Unsupported input for Project resource');
|
||||
}
|
||||
|
||||
$project = $this->entityStateProcessor->process($project, $operation, $uriVariables, $context);
|
||||
@@ -72,13 +72,27 @@ class ProjectStateProcessor implements ProcessorInterface
|
||||
return $project;
|
||||
}
|
||||
|
||||
private function getProjectFromUpdate(ProjectUpdationDto $data, array $uriVariables): Project
|
||||
private function getProjectFromUpdate(ProjectUpdationDto $data, array $context): Project
|
||||
{
|
||||
$data = $this->autoMapper->map($uriVariables, $data);
|
||||
|
||||
/** @var Project */
|
||||
$project = $this->autoMapper->map($data, Project::class);
|
||||
$project = $this->autoMapper->map($context['previous_data'], Project::class);
|
||||
|
||||
return $project;
|
||||
$actor = $this->security->getUser();
|
||||
|
||||
if (!$actor) {
|
||||
throw new AuthenticationException();
|
||||
}
|
||||
|
||||
if ($data->status !== $project->getStatus()) {
|
||||
if (!$this->projectService->canTransition($actor, $project, $data->status)) {
|
||||
throw new AccessDeniedException(\sprintf(
|
||||
"Cannot move the Project from status '%s' to '%s'",
|
||||
$project->getStatus()->value,
|
||||
$data->status->value
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
return $this->autoMapper->map($data, $project);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user