Skip to content
Advertisement

Symfony Workflow Component and Security Voters?

TL;DR: how can you add custom constraints (i.e. security voters) to transitions?

My application needs some workflow management system, so I’d like to try Symfony’s new Workflow Component. Let’s take a Pull Request workflow as an example.

In this example, only states and their transitions are describes. But what if I want to add other constraints to this workflow? I can image some constraints:

  • Only admins can accept Pull Request
  • Users can only reopen their own Pull Request
  • Users can not reopen PR’s older than 1 year

While you can use Events in this case, I don’t think that’s the best way to handle it, because an event is fired after $workflow->apply(). I want to know beforehand if a user is allowed to change the state, so I can hide or disable the button. (not like this).

The LexikWorkflowBundle solved this problem partially, by adding roles to the steps (transitions). Switching to this bundle might be a good idea, but I’d like to figure out how I can solve this problem without.

What is the best way to add custom entity constraints (‘PR older than 1 year can’t be reopened‘) and security constraints (‘only admins can accept PR’s‘, maybe by using Symfony’s Security Voters) to transitions?

Update: To clarify: I want to add permission control to my workflow, but that doesn’t necessarily mean I want to tightly couple it to the Workflow Component. I’d like to stick to good practices, so the given solution should respect the single responsibility principle.

Advertisement

Answer

The best way I found was implementing the AuthorizationChecker in the Workflow’s GuardListener.

The demo application gives a good example:

namespace AcmeDemoBundleEntityListener;

use SymfonyComponentEventDispatcherEventSubscriberInterface;
use SymfonyComponentSecurityCoreAuthorizationAuthorizationChecker;
use SymfonyComponentWorkflowEventGuardEvent;

class GuardListener implements EventSubscriberInterface
{
    public function __construct(AuthorizationCheckerInterface $checker)
    {
        $this->checker = $checker;
    }
    public function onTransition(GuardEvent $event)
    {
        // For all action, user should be logger
        if (!$this->checker->isGranted('IS_AUTHENTICATED_FULLY')) {
            $event->setBlocked(true);
        }
    }
    public function onTransitionJournalist(GuardEvent $event)
    {
        if (!$this->checker->isGranted('ROLE_JOURNALIST')) {
            $event->setBlocked(true);
        }
    }
    public function onTransitionSpellChecker(GuardEvent $event)
    {
        if (!$this->checker->isGranted('ROLE_SPELLCHECKER')) {
            $event->setBlocked(true);
        }
    }
    public static function getSubscribedEvents()
    {
        return [
            'workflow.article.guard' => 'onTransition',
            'workflow.article.guard.journalist_approval' => 'onTransitionJournalist',
            'workflow.article.guard.spellchecker_approval' => 'onTransitionSpellChecker',
        ];
    }
User contributions licensed under: CC BY-SA
7 People found this is helpful
Advertisement