How to use voters/permissions on Symfony messenger async message handler?

1.8k views Asked by At

I am developing an application that have a Symfony Messenger component installed to handle async messages. The handler of message need to check some permissions for some particulars users, like if one determinate user should receive an email with information if have edition permissions for example.

To achieve that we use Symfony voters, but when we haven't any user logged into the system like in console commands and async messages is very annoying. What is the best solution to that?

Thank you so much

1

There are 1 answers

4
Mikhail Prosalov On BEST ANSWER

I would probably prefer to check user's permissions before dispatching a message, but let's think how we can approach if it's not a suitable case.

In order to check user permissions, you need to authenticate a user. But in case you're consuming a message asynchronously or executing a console command it's not straightforward, as you don't have an actual user. However, you can pass user id with your message or to a console command.

Let me share my idea of a simple solution for Symfony Messenger. In the Symfony Messenger, there is a concept of Stamps, which allows you to add metadata to your message. In our case it would be useful to pass a user id with a message, so we can authenticate a user within the message handling process.

Let's create a custom stamp to hold a user id. It's a simple PHP class, so no need to register it as a service.

<?php

namespace App\Messenger\Stamp;

use Symfony\Component\Messenger\Stamp\StampInterface;

class AuthenticationStamp implements StampInterface
{
    private $userId;

    public function __construct(string $userId)
    {
        $this->userId = $userId;
    }

    public function getUserId(): string
    {
        return $this->userId;
    }
}

Now we can add the stamp to a message.

$message = new SampleMessage($payload);
$this->messageBus->dispatch(
    (new Envelope($message))
        ->with(new AuthenticationStamp($userId))
);

We need to receive and handle the stamp in order to authenticate a user. Symfony Messenger has a concept of Middlewares, so let's create one to handle stamp when we receive a message by a worker. It would check if the message contains the AuthenticationStamp and authenticate a user if the user is not authenticated at the moment.

<?php

namespace App\Messenger\Middleware;

use App\Messenger\Stamp\AuthenticationStamp;
use App\Repository\UserRepositoryInterface;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\Middleware\MiddlewareInterface;
use Symfony\Component\Messenger\Middleware\StackInterface;
use Symfony\Component\Security\Core\Authentication\Token\AnonymousToken;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;

class AuthenticationMiddleware implements MiddlewareInterface
{
    private $tokenStorage;
    private $userRepository;

    public function __construct(TokenStorageInterface $tokenStorage, UserRepositoryInterface $userRepository)
    {
        $this->tokenStorage = $tokenStorage;
        $this->userRepository = $userRepository;
    }

    public function handle(Envelope $envelope, StackInterface $stack): Envelope
    {
        /** @var AuthenticationStamp|null $authenticationStamp */
        if ($authenticationStamp = $envelope->last(AuthenticationStamp::class)) {
            $userId = $authenticationStamp->getUserId();

            $token = $this->tokenStorage->getToken();
            if (null === $token || $token instanceof AnonymousToken) {
                $user = $this->userRepository->find($userId);
                if ($user) {
                    $this->tokenStorage->setToken(new UsernamePasswordToken(
                        $user,
                        null,
                        'provider',
                        $user->getRoles())
                    );
                }
            }
        }

        return $stack->next()->handle($envelope, $stack);
    }
}

Let's register it as a service (or autowire) and include into the messenger configuration definition.

framework:
  messenger:
    buses:
      messenger.bus.default:
        middleware:
          - 'App\Messenger\Middleware\AuthenticationMiddleware'

That's pretty much it. Now you should be able to use your regular way to check user's permissions, for example, voters.

As for console command, I would go for an authentication service, which would authenticate a user if the user id is passed to a command.