sajad torkamani

In a nutshell

Symfony’s Voters help you implement authorization in your web applications – the process of checking that a user has the right permission to do a particular action (e.g., publish an article).

Creating a Voter (Permission checker)

The access checking code

Suppose you have a Post object and you need to decide whether the authenticated user can view or edit the post. In your controller, you might have code like this:

// src/Controller/PostController.php

// ...
class PostController extends AbstractController
{
    #[Route('/posts/{id}', name: 'post_show')]
    public function show($id): Response
    {
        // get a Post object - e.g. query for it
        $post = ...;

        // check for "view" access: calls all voters
        $this->denyAccessUnlessGranted('view', $post);

        // ...
    }

    #[Route('/posts/{id}/edit', name: 'post_edit')]
    public function edit($id): Response
    {
        // get a Post object - e.g. query for it
        $post = ...;

        // check for "edit" access: calls all voters
        $this->denyAccessUnlessGranted('edit', $post);

        // ...
    }
}

The denyAccessUnlessGranted (and isGranted) method will invoke Symfony’s “voter” system. At the moment, no voters will vote on whether the user can “view” or “edit” a Post. But you can implement your own voter that decides this using whatever logic you want.

Creating the custom voter

// src/Security/PostVoter.php
namespace App\Security;

use App\Entity\Post;
use App\Entity\User;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;

class PostVoter extends Voter
{
    // these strings are just invented: you can use anything
    const VIEW = 'view';
    const EDIT = 'edit';

    protected function supports(string $attribute, mixed $subject): bool
    {
        // if the attribute isn't one we support, return false
        if (!in_array($attribute, [self::VIEW, self::EDIT])) {
            return false;
        }

        // only vote on `Post` objects
        if (!$subject instanceof Post) {
            return false;
        }

        return true;
    }

    protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
    {
        $user = $token->getUser();

        if (!$user instanceof User) {
            // the user must be logged in; if not, deny access
            return false;
        }

        // you know $subject is a Post object, thanks to `supports()`
        /** @var Post $post */
        $post = $subject;

        return match($attribute) {
            self::VIEW => $this->canView($post, $user),
            self::EDIT => $this->canEdit($post, $user),
            default => throw new \LogicException('This code should not be reached!')
        };
    }

    private function canView(Post $post, User $user): bool
    {
        // if they can edit, they can view
        if ($this->canEdit($post, $user)) {
            return true;
        }

        // the Post object could have, for example, a method `isPrivate()`
        return !$post->isPrivate();
    }

    private function canEdit(Post $post, User $user): bool
    {
        // this assumes that the Post object has a `getOwner()` method
        return $user === $post->getOwner();
    }
}

Custom voters must implement the VoterInterface or extend the abstract class Voter.

Voter::supports

You use the Voter::supports method to determine whether the current voter applies to the attribute and subject combination:

abstract protected function supports(string $attribute, mixed $subject): bool;

If you return true, the voteOnAtttribute method is called. Otherwise, your voter is done and other voters (if any) will be processed.

Voter::voteOnAttribute

If your voter returns true from supports(), this method is called. You return true to allow access and false to deny access.

  • $attribute: The string passed to denyAccessUnlessGranted or isGranted() (e.g., the "view" string in $this->denyAccessUnlessGranted('view', $post)).
  • $subject: The object that is being voted on (e.g., the $post object in $this->denyAccessUnlessGranted('view', $post)).
  • $token: Can be used to retrieve the authenticated user (if any).

Check for roles inside a Voter

// src/Security/PostVoter.php

// ...
use Symfony\Bundle\SecurityBundle\Security;

class PostVoter extends Voter
{
    // ...

    private $security;

    public function __construct(Security $security)
    {
        $this->security = $security;
    }

    protected function voteOnAttribute($attribute, mixed $subject, TokenInterface $token): bool
    {
        // ...

        // ROLE_SUPER_ADMIN can do anything! The power!
        if ($this->security->isGranted('ROLE_SUPER_ADMIN')) {
            return true;
        }

        // ... all the normal voter logic
    }
}

Access Decision Strategies

The access decision manager uses a “strategy” to carry out permission checks. There are four strategies available:

  • affirmative (default) – This grants access to a resource as soon as there is one voter granting access.
  • consensus – This grants access if more voters grant access than deny (i.e., there is a consensus). In case of a tie, the decision is based on the allow_if_equal_granted_denied config options (defaults to true).
  • unanimous – This grants access only if there is no voter denying access.
  • priority – This grants or denies access by the first voter that does not abstain, based on their service priority.

Regardless of the strategy configured, if all voters abstain from voting (e.g., none return true in the Voter::supports method), the decision will be based on the allow_if_all_abstain config option (defaults to false).

You can configure the strategy in config/packages/security.yaml:

# config/packages/security.yaml
security:
    access_decision_manager:
        strategy: unanimous
        allow_if_all_abstain: false

You can use a custom access decision strategy or even a custom access decision manager, if required.

Other notes

  • Each time you call the isGranted() or denyAccessUnlessGranted() methods, or use access controls, Symfony will call all your voters. Symfony will take the responses from all your voters and make a final decision – whether to allow or deny access to a resource.
  • If you have a lot of voters and calling them all leads to performance issues, you can make your voters implement the CacheableVoterInterface to cache the permissions checks.

Sources

Tagged: Symfony