How to use voters/permissions on Symfony messenger async message handler? [closed]
Asked Answered
R

1

6

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, or if one has 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.

How can I check for user permissions when consuming messages asynchronously?

Revitalize answered 7/10, 2020 at 8:15 Comment(0)
A
14

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.

Arbitrator answered 7/10, 2020 at 14:49 Comment(4)
Thanks a lot for that complete and accurate response. The middleware is, so far, another option and probably the best for the major cases. I agree with you about is better check the permissions before send the message to handler. In my concrete case we generate an event with an object and that must be notified by email to a concrete users that have a certain number of permissions. In that case, I can't validate permissions because there aren't any context. I am thinking the possibility of create a TokenInterface and pass it directly to DecisionManagerRevitalize
The concept of Voters designed to check permissions for an authenticated user. Not sure it's the perfect fit to check permissions for multiple users before sending an email. I would implement a service to filter out users, eligible for the email notification. Or fetch eligible users from a repository. Not sure which would be the best option, as I don't have enough context on the application.Arbitrator
@MikhailProsalov 's answer is excellent. I wanted to add only that in my use case, I needed to add the middleware to the command_bus.middleware key (in addition to the router_context middleware that I was using. This was because in messenger, the consume command is running in bin/console messenger:consumeCarlile
also - with Symfony 5.4 the UserPasswordToken args are changed (deprecated and changed in Symfony 6) so that you only need 3 args: $user, $firewallName, and $roles. So I have ($user, 'main', $user->getRoles())Carlile

© 2022 - 2024 — McMap. All rights reserved.