diff --git a/src/Bundle/ChillMainBundle/Controller/WorkflowController.php b/src/Bundle/ChillMainBundle/Controller/WorkflowController.php index 646a69438..e87e8e36d 100644 --- a/src/Bundle/ChillMainBundle/Controller/WorkflowController.php +++ b/src/Bundle/ChillMainBundle/Controller/WorkflowController.php @@ -11,8 +11,10 @@ declare(strict_types=1); namespace Chill\MainBundle\Controller; +use Chill\MainBundle\Entity\User; use Chill\MainBundle\Entity\Workflow\EntityWorkflow; use Chill\MainBundle\Entity\Workflow\EntityWorkflowComment; +use Chill\MainBundle\Entity\Workflow\EntityWorkflowStep; use Chill\MainBundle\Form\EntityWorkflowCommentType; use Chill\MainBundle\Form\WorkflowStepType; use Chill\MainBundle\Pagination\PaginatorFactory; @@ -24,10 +26,12 @@ use Doctrine\ORM\EntityManagerInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\Form\Extension\Core\Type\FormType; use Symfony\Component\Form\Extension\Core\Type\SubmitType; +use Symfony\Component\HttpFoundation\Exception\BadRequestException; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\Routing\Annotation\Route; +use Symfony\Component\Security\Core\Exception\AccessDeniedException; use Symfony\Component\Validator\Validator\ValidatorInterface; use Symfony\Component\Workflow\Registry; use Symfony\Component\Workflow\TransitionBlocker; @@ -84,8 +88,7 @@ class WorkflowController extends AbstractController ->setRelatedEntityId($request->query->getInt('entityId')) ->setWorkflowName($request->query->get('workflow')) ->addSubscriberToStep($this->getUser()) - ->addSubscriberToFinal($this->getUser()) - ; + ->addSubscriberToFinal($this->getUser()); $errors = $this->validator->validate($entityWorkflow, null, ['creation']); @@ -136,6 +139,37 @@ class WorkflowController extends AbstractController ]); } + /** + * @Route("/{_locale}/main/workflow-step/{id}/access_key", name="chill_main_workflow_grant_access_by_key") + */ + public function getAccessByAccessKey(EntityWorkflowStep $entityWorkflowStep, Request $request): Response + { + if (null === $accessKey = $request->query->get('accessKey', null)) { + throw new BadRequestException('accessKey is missing'); + } + + if (!$this->getUser() instanceof User) { + throw new AccessDeniedException('Not a valid user'); + } + + dump($accessKey); + dump($entityWorkflowStep->getAccessKey()); + + if ($entityWorkflowStep->getAccessKey() !== $accessKey) { + throw new AccessDeniedException('Access key is invalid'); + } + + if (!$entityWorkflowStep->isWaitingForTransition()) { + $this->addFlash('error', $this->translator->trans('workflow.Steps is not waiting for transition. Maybe someone apply the transition before you ?')); + } else { + $entityWorkflowStep->addDestUserByAccessKey($this->getUser()); + $this->entityManager->flush(); + $this->addFlash('success', $this->translator->trans('workflow.You get access to this step')); + } + + return $this->redirectToRoute('chill_main_workflow_show', ['id' => $entityWorkflowStep->getEntityWorkflow()->getId()]); + } + /** * @Route("/{_locale}/main/workflow/list/dest", name="chill_main_workflow_list_dest") */ diff --git a/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflowStep.php b/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflowStep.php index bde2f28ac..590380749 100644 --- a/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflowStep.php +++ b/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflowStep.php @@ -27,6 +27,11 @@ use function in_array; */ class EntityWorkflowStep { + /** + * @ORM\Column(type="text", nullable=false) + */ + private string $accessKey; + /** * @ORM\Column(type="text", options={"default": ""}) */ @@ -48,6 +53,12 @@ class EntityWorkflowStep */ private Collection $destUser; + /** + * @ORM\ManyToMany(targetEntity=User::class) + * @ORM\JoinTable(name="chill_main_workflow_entity_step_user_by_accesskey") + */ + private Collection $destUserByAccessKey; + /** * @ORM\ManyToOne(targetEntity=EntityWorkflow::class, inversedBy="steps") */ @@ -101,14 +112,10 @@ class EntityWorkflowStep */ private ?string $transitionByEmail = null; - /** - * @ORM\Column(type="text", nullable=false) - */ - private string $accessKey; - public function __construct() { $this->destUser = new ArrayCollection(); + $this->destUserByAccessKey = new ArrayCollection(); $this->accessKey = bin2hex(openssl_random_pseudo_bytes(32)); } @@ -133,6 +140,37 @@ class EntityWorkflowStep return $this; } + public function addDestUserByAccessKey(User $user): self + { + if (!$this->destUserByAccessKey->contains($user)) { + $this->destUserByAccessKey[] = $user; + $this->getEntityWorkflow() + ->addSubscriberToFinal($user) + ->addSubscriberToStep($user); + } + + return $this; + } + + public function getAccessKey(): string + { + return $this->accessKey; + } + + /** + * get all the users which are allowed to apply a transition: those added manually, and + * those added automatically bu using an access key. + */ + public function getAllDestUser(): Collection + { + return new ArrayCollection( + [ + ...$this->getDestUser()->toArray(), + ...$this->getDestUserByAccessKey()->toArray(), + ] + ); + } + public function getComment(): string { return $this->comment; @@ -149,13 +187,21 @@ class EntityWorkflowStep } /** - * @return ArrayCollection|Collection + * get dest users added by the creator. + * + * You should **not** rely on this method to get all users which are able to + * apply a transition on this step. Use @see{EntityWorkflowStep::getAllDestUser} instead. */ - public function getDestUser() + public function getDestUser(): collection { return $this->destUser; } + public function getDestUserByAccessKey(): Collection + { + return $this->destUserByAccessKey; + } + public function getEntityWorkflow(): ?EntityWorkflow { return $this->entityWorkflow; @@ -206,6 +252,19 @@ class EntityWorkflowStep return $this->freezeAfter; } + public function isWaitingForTransition(): bool + { + if (null !== $this->transitionAfter) { + return false; + } + + if ($this->isFinal()) { + return false; + } + + return true; + } + public function removeDestEmail(string $email): self { $this->destEmail = array_filter($this->destEmail, static function (string $existing) use ($email) { @@ -222,6 +281,13 @@ class EntityWorkflowStep return $this; } + public function removeDestUserByAccessKey(User $user): self + { + $this->destUserByAccessKey->removeElement($user); + + return $this; + } + public function setComment(?string $comment): EntityWorkflowStep { $this->comment = (string) $comment; diff --git a/src/Bundle/ChillMainBundle/Resources/views/Workflow/_decision.html.twig b/src/Bundle/ChillMainBundle/Resources/views/Workflow/_decision.html.twig index 64482c067..5ae454359 100644 --- a/src/Bundle/ChillMainBundle/Resources/views/Workflow/_decision.html.twig +++ b/src/Bundle/ChillMainBundle/Resources/views/Workflow/_decision.html.twig @@ -82,13 +82,23 @@

{{ 'workflow.This workflow is finalized'|trans }}

{% else %}

{{ 'workflow.You are not allowed to apply a transition on this workflow'|trans }}

-

{{ 'workflow.Only those users are allowed'|trans }}:

+ {% if entity_workflow.currentStep.destUser|length > 0 %} +

{{ 'workflow.Only those users are allowed'|trans }} :

+ + {% endif %} - + {% if entity_workflow.currentStep.destUserByAccessKey|length > 0 %} +

{{ 'workflow.Those users are also granted to apply a transition by using an access key'|trans }} :

+ + {% endif %} {% endif %} diff --git a/src/Bundle/ChillMainBundle/Resources/views/Workflow/_history.html.twig b/src/Bundle/ChillMainBundle/Resources/views/Workflow/_history.html.twig index e40e82dab..dda309da0 100644 --- a/src/Bundle/ChillMainBundle/Resources/views/Workflow/_history.html.twig +++ b/src/Bundle/ChillMainBundle/Resources/views/Workflow/_history.html.twig @@ -69,15 +69,26 @@ {% endif %} - {% if loop.last and step.destUser|length > 0 %} + {% if loop.last and step.allDestUser|length > 0 %}
-

{{ 'workflow.Users allowed to apply transition'|trans }} :

- + {% if step.destUser|length > 0 %} +

{{ 'workflow.Users allowed to apply transition'|trans }} :

+ + {% endif %} + + {% if step.destUserByAccessKey|length > 0 %} +

{{ 'workflow.Those users are also granted to apply a transition by using an access key'|trans }} :

+ + {% endif %}
{% endif %} diff --git a/src/Bundle/ChillMainBundle/Workflow/EventSubscriber/EntityWorkflowTransitionEventSubscriber.php b/src/Bundle/ChillMainBundle/Workflow/EventSubscriber/EntityWorkflowTransitionEventSubscriber.php index 1fc56e898..7074be14d 100644 --- a/src/Bundle/ChillMainBundle/Workflow/EventSubscriber/EntityWorkflowTransitionEventSubscriber.php +++ b/src/Bundle/ChillMainBundle/Workflow/EventSubscriber/EntityWorkflowTransitionEventSubscriber.php @@ -72,7 +72,7 @@ class EntityWorkflowTransitionEventSubscriber implements EventSubscriberInterfac return; } - if (!$entityWorkflow->getCurrentStep()->getDestUser()->contains($this->security->getUser())) { + if (!$entityWorkflow->getCurrentStep()->getAllDestUser()->contains($this->security->getUser())) { if (!$event->getMarking()->has('initial')) { $event->addTransitionBlocker(new TransitionBlocker( 'workflow.You are not allowed to apply a transition on this workflow. Only those users are allowed: %users%', @@ -80,7 +80,7 @@ class EntityWorkflowTransitionEventSubscriber implements EventSubscriberInterfac [ '%users%' => implode( ', ', - $entityWorkflow->getCurrentStep()->getDestUser()->map(function (User $u) { + $entityWorkflow->getCurrentStep()->getAllDestUser()->map(function (User $u) { return $this->userRender->renderString($u, []); })->toArray() ), diff --git a/src/Bundle/ChillMainBundle/migrations/Version20220223171457.php b/src/Bundle/ChillMainBundle/migrations/Version20220223171457.php index c79954fa3..b62f4a88a 100644 --- a/src/Bundle/ChillMainBundle/migrations/Version20220223171457.php +++ b/src/Bundle/ChillMainBundle/migrations/Version20220223171457.php @@ -1,5 +1,12 @@ addSql('ALTER TABLE chill_main_workflow_entity_step DROP accessKey'); + $this->addSql('DROP TABLE chill_main_workflow_entity_step_user_by_accesskey'); + } + public function getDescription(): string { - return ''; + return 'Add access key to EntityWorkflowStep'; } public function up(Schema $schema): void @@ -31,10 +41,11 @@ final class Version20220223171457 extends AbstractMigration UPDATE chill_main_workflow_entity_step SET accessKey = randoms.random_word FROM randoms WHERE chill_main_workflow_entity_step.id = randoms.id'); $this->addSql('ALTER TABLE chill_main_workflow_entity_step ALTER accessKey DROP DEFAULT '); $this->addSql('ALTER TABLE chill_main_workflow_entity_step ALTER accessKey SET NOT NULL'); - } - public function down(Schema $schema): void - { - $this->addSql('ALTER TABLE chill_main_workflow_entity_step DROP accessKey'); + $this->addSql('CREATE TABLE chill_main_workflow_entity_step_user_by_accesskey (entityworkflowstep_id INT NOT NULL, user_id INT NOT NULL, PRIMARY KEY(entityworkflowstep_id, user_id))'); + $this->addSql('CREATE INDEX IDX_8296D8397E6AF9D4 ON chill_main_workflow_entity_step_user_by_accesskey (entityworkflowstep_id)'); + $this->addSql('CREATE INDEX IDX_8296D839A76ED395 ON chill_main_workflow_entity_step_user_by_accesskey (user_id)'); + $this->addSql('ALTER TABLE chill_main_workflow_entity_step_user_by_accesskey ADD CONSTRAINT FK_8296D8397E6AF9D4 FOREIGN KEY (entityworkflowstep_id) REFERENCES chill_main_workflow_entity_step (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE chill_main_workflow_entity_step_user_by_accesskey ADD CONSTRAINT FK_8296D839A76ED395 FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); } } diff --git a/src/Bundle/ChillMainBundle/translations/messages.fr.yml b/src/Bundle/ChillMainBundle/translations/messages.fr.yml index 157295d46..86f906413 100644 --- a/src/Bundle/ChillMainBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillMainBundle/translations/messages.fr.yml @@ -400,6 +400,9 @@ workflow: Delete workflow ?: Supprimer le workflow ? Are you sure you want to delete this workflow ?: Êtes-vous sûr·e de vouloir supprimer ce workflow ? Delete workflow: Supprimer le workflow + Steps is not waiting for transition. Maybe someone apply the transition before you ?: L'étape que vous cherchez a déjà été modifiée par un autre utilisateur. Peut-être quelqu'un a-t-il modifié cette étape avant vous ? + You get access to this step: Vous avez acquis les droits pour appliquer une transition sur ce workflow. + Those users are also granted to apply a transition by using an access key: Ces utilisateurs peuvent également valider cette étape, grâce à un lien d'accès Subscribe final: Recevoir une notification à l'étape finale Subscribe all steps: Recevoir une notification à chaque étape