Symfony Workflow Component and Security Voters?
Asked Answered
A

1

0

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.

Argol answered 10/1, 2017 at 10:18 Comment(11)
Yagni. Workflow component should be responsible for workflow only. Think about it other way round - transition is one of the constraint you are using in your application. Apply other constrains before or after $workflow->can is resolved, same way as you would use it in combination with ACL, for example.Announcer
I understand the other constraints should not be tightly coupled with the Workflow Component. But on the other hand, I want to have a single place where I validate all constraints (workflow, entity, security) for each transition to prevent spaghetti code in my controllers.Argol
What stops you to create such place as a service which uses workflow as one of the constraints?Announcer
Technically, nothing. But I suppose I'm not the only one with this question, so there must be someone who figured out the best way to solve this problem.Argol
Even if you don't take single responsibility principle as the best solution, it is a good one. After all Bolivar does its best with a lone rider. Let the workflow do the transition logic, write your own constrains specific to your business logic, and compose them in a single service if you like. I wouldn't even mess with workflow Events.Announcer
I too am a bit skeptical about mixing workflow and permissions but if you really want to check permissions from inside of a workflow object then inject security.authorization_checker which will give you access to the voters via the isGranted method.Diskson
@Diskson I'm not sure 'mixing' is the right word. I don't want to mix them, I just want to use them both. I want to check if a certain transition is valid (workflow) and if that user is allowed to create that transition (security). I'm looking for a way that meets best practices.Argol
Another approach might be to pass the workflow to the voter? Just a thought. Probably does not make sense. Let us know how it turns out.Diskson
I've thought of that as well, but that won't work. In case of a workflow, I don't want to validate the entity but the transition.Argol
If I do not misunderstand your use case, an event is exactly what you want to use here. Inside your event listener, you check some preconditions (which could also be a call to a voter) and then mark it as blocked (take a look again at the example you linked to to see how this can be done).Counterreply
I'm now trying to implement it by using GuardEvent. I'll let you know how that works out.Argol
A
5

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

The demo application gives a good example:

namespace Acme\DemoBundle\Entity\Listener;

use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Security\Core\Authorization\AuthorizationChecker;
use Symfony\Component\Workflow\Event\GuardEvent;

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',
        ];
    }
Argol answered 20/2, 2017 at 10:34 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.