workflow: allow a user to get access to validation step by an access key

This commit is contained in:
Julien Fastré 2022-02-24 12:17:13 +01:00
parent 08f9819453
commit ff1ff8f5bb
7 changed files with 167 additions and 32 deletions

View File

@ -11,8 +11,10 @@ declare(strict_types=1);
namespace Chill\MainBundle\Controller; namespace Chill\MainBundle\Controller;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Entity\Workflow\EntityWorkflow; use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
use Chill\MainBundle\Entity\Workflow\EntityWorkflowComment; use Chill\MainBundle\Entity\Workflow\EntityWorkflowComment;
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStep;
use Chill\MainBundle\Form\EntityWorkflowCommentType; use Chill\MainBundle\Form\EntityWorkflowCommentType;
use Chill\MainBundle\Form\WorkflowStepType; use Chill\MainBundle\Form\WorkflowStepType;
use Chill\MainBundle\Pagination\PaginatorFactory; use Chill\MainBundle\Pagination\PaginatorFactory;
@ -24,10 +26,12 @@ use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Form\Extension\Core\Type\FormType; use Symfony\Component\Form\Extension\Core\Type\FormType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType; use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\HttpFoundation\Exception\BadRequestException;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use Symfony\Component\Validator\Validator\ValidatorInterface; use Symfony\Component\Validator\Validator\ValidatorInterface;
use Symfony\Component\Workflow\Registry; use Symfony\Component\Workflow\Registry;
use Symfony\Component\Workflow\TransitionBlocker; use Symfony\Component\Workflow\TransitionBlocker;
@ -84,8 +88,7 @@ class WorkflowController extends AbstractController
->setRelatedEntityId($request->query->getInt('entityId')) ->setRelatedEntityId($request->query->getInt('entityId'))
->setWorkflowName($request->query->get('workflow')) ->setWorkflowName($request->query->get('workflow'))
->addSubscriberToStep($this->getUser()) ->addSubscriberToStep($this->getUser())
->addSubscriberToFinal($this->getUser()) ->addSubscriberToFinal($this->getUser());
;
$errors = $this->validator->validate($entityWorkflow, null, ['creation']); $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") * @Route("/{_locale}/main/workflow/list/dest", name="chill_main_workflow_list_dest")
*/ */

View File

@ -27,6 +27,11 @@ use function in_array;
*/ */
class EntityWorkflowStep class EntityWorkflowStep
{ {
/**
* @ORM\Column(type="text", nullable=false)
*/
private string $accessKey;
/** /**
* @ORM\Column(type="text", options={"default": ""}) * @ORM\Column(type="text", options={"default": ""})
*/ */
@ -48,6 +53,12 @@ class EntityWorkflowStep
*/ */
private Collection $destUser; 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") * @ORM\ManyToOne(targetEntity=EntityWorkflow::class, inversedBy="steps")
*/ */
@ -101,14 +112,10 @@ class EntityWorkflowStep
*/ */
private ?string $transitionByEmail = null; private ?string $transitionByEmail = null;
/**
* @ORM\Column(type="text", nullable=false)
*/
private string $accessKey;
public function __construct() public function __construct()
{ {
$this->destUser = new ArrayCollection(); $this->destUser = new ArrayCollection();
$this->destUserByAccessKey = new ArrayCollection();
$this->accessKey = bin2hex(openssl_random_pseudo_bytes(32)); $this->accessKey = bin2hex(openssl_random_pseudo_bytes(32));
} }
@ -133,6 +140,37 @@ class EntityWorkflowStep
return $this; 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 public function getComment(): string
{ {
return $this->comment; 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; return $this->destUser;
} }
public function getDestUserByAccessKey(): Collection
{
return $this->destUserByAccessKey;
}
public function getEntityWorkflow(): ?EntityWorkflow public function getEntityWorkflow(): ?EntityWorkflow
{ {
return $this->entityWorkflow; return $this->entityWorkflow;
@ -206,6 +252,19 @@ class EntityWorkflowStep
return $this->freezeAfter; 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 public function removeDestEmail(string $email): self
{ {
$this->destEmail = array_filter($this->destEmail, static function (string $existing) use ($email) { $this->destEmail = array_filter($this->destEmail, static function (string $existing) use ($email) {
@ -222,6 +281,13 @@ class EntityWorkflowStep
return $this; return $this;
} }
public function removeDestUserByAccessKey(User $user): self
{
$this->destUserByAccessKey->removeElement($user);
return $this;
}
public function setComment(?string $comment): EntityWorkflowStep public function setComment(?string $comment): EntityWorkflowStep
{ {
$this->comment = (string) $comment; $this->comment = (string) $comment;

View File

@ -82,13 +82,23 @@
<p>{{ 'workflow.This workflow is finalized'|trans }}</p> <p>{{ 'workflow.This workflow is finalized'|trans }}</p>
{% else %} {% else %}
<p>{{ 'workflow.You are not allowed to apply a transition on this workflow'|trans }}</p> <p>{{ 'workflow.You are not allowed to apply a transition on this workflow'|trans }}</p>
<p>{{ 'workflow.Only those users are allowed'|trans }}:</p> {% if entity_workflow.currentStep.destUser|length > 0 %}
<p><b>{{ 'workflow.Only those users are allowed'|trans }}&nbsp;:</b></p>
<ul>
{% for u in entity_workflow.currentStep.destUser -%}
<li>{{ u|chill_entity_render_box }}</li>
{%- endfor %}
</ul>
{% endif %}
<ul> {% if entity_workflow.currentStep.destUserByAccessKey|length > 0 %}
{% for u in entity_workflow.currentStep.destUser -%} <p><b>{{ 'workflow.Those users are also granted to apply a transition by using an access key'|trans }}&nbsp;:</b></p>
<li>{{ u|chill_entity_render_box }}</li> <ul>
{%- endfor %} {% for u in entity_workflow.currentStep.destUserByAccessKey %}
</ul> <li>{{ u|chill_entity_render_box }}</li>
{% endfor %}
</ul>
{% endif %}
{% endif %} {% endif %}
</div> </div>

View File

@ -69,15 +69,26 @@
</blockquote> </blockquote>
</div> </div>
{% endif %} {% endif %}
{% if loop.last and step.destUser|length > 0 %} {% if loop.last and step.allDestUser|length > 0 %}
<div class="item-row separator"> <div class="item-row separator">
<div> <div>
<p><b>{{ 'workflow.Users allowed to apply transition'|trans }}&nbsp;: </b></p> {% if step.destUser|length > 0 %}
<ul> <p><b>{{ 'workflow.Users allowed to apply transition'|trans }}&nbsp;: </b></p>
{% for u in step.destUser %} <ul>
<li>{{ u|chill_entity_render_box }}</li> {% for u in step.destUser %}
{% endfor %} <li>{{ u|chill_entity_render_box }}</li>
</ul> {% endfor %}
</ul>
{% endif %}
{% if step.destUserByAccessKey|length > 0 %}
<p><b>{{ 'workflow.Those users are also granted to apply a transition by using an access key'|trans }}&nbsp;:</b></p>
<ul>
{% for u in step.destUserByAccessKey %}
<li>{{ u|chill_entity_render_box }}</li>
{% endfor %}
</ul>
{% endif %}
</div> </div>
</div> </div>
{% endif %} {% endif %}

View File

@ -72,7 +72,7 @@ class EntityWorkflowTransitionEventSubscriber implements EventSubscriberInterfac
return; return;
} }
if (!$entityWorkflow->getCurrentStep()->getDestUser()->contains($this->security->getUser())) { if (!$entityWorkflow->getCurrentStep()->getAllDestUser()->contains($this->security->getUser())) {
if (!$event->getMarking()->has('initial')) { if (!$event->getMarking()->has('initial')) {
$event->addTransitionBlocker(new TransitionBlocker( $event->addTransitionBlocker(new TransitionBlocker(
'workflow.You are not allowed to apply a transition on this workflow. Only those users are allowed: %users%', '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( '%users%' => implode(
', ', ', ',
$entityWorkflow->getCurrentStep()->getDestUser()->map(function (User $u) { $entityWorkflow->getCurrentStep()->getAllDestUser()->map(function (User $u) {
return $this->userRender->renderString($u, []); return $this->userRender->renderString($u, []);
})->toArray() })->toArray()
), ),

View File

@ -1,5 +1,12 @@
<?php <?php
/**
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
declare(strict_types=1); declare(strict_types=1);
namespace Chill\Migrations\Main; namespace Chill\Migrations\Main;
@ -7,14 +14,17 @@ namespace Chill\Migrations\Main;
use Doctrine\DBAL\Schema\Schema; use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration; use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20220223171457 extends AbstractMigration final class Version20220223171457 extends AbstractMigration
{ {
public function down(Schema $schema): void
{
$this->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 public function getDescription(): string
{ {
return ''; return 'Add access key to EntityWorkflowStep';
} }
public function up(Schema $schema): void 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'); 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 DROP DEFAULT ');
$this->addSql('ALTER TABLE chill_main_workflow_entity_step ALTER accessKey SET NOT NULL'); $this->addSql('ALTER TABLE chill_main_workflow_entity_step ALTER accessKey SET NOT NULL');
}
public function down(Schema $schema): void $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('ALTER TABLE chill_main_workflow_entity_step DROP accessKey'); $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');
} }
} }

View File

@ -400,6 +400,9 @@ workflow:
Delete workflow ?: Supprimer le 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 ? Are you sure you want to delete this workflow ?: Êtes-vous sûr·e de vouloir supprimer ce workflow ?
Delete workflow: Supprimer le 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 final: Recevoir une notification à l'étape finale
Subscribe all steps: Recevoir une notification à chaque étape Subscribe all steps: Recevoir une notification à chaque étape