Skip to content
  • About
    • What is Symfony?
    • Community
    • News
    • Contributing
    • Support
  • Documentation
    • Symfony Docs
    • Symfony Book
    • Screencasts
    • Symfony Bundles
    • Symfony Cloud
    • Training
  • Services
    • SensioLabs Professional services to help you with Symfony
    • Platform.sh for Symfony Best platform to deploy Symfony apps
    • SymfonyInsight Automatic quality checks for your apps
    • Symfony Certification Prove your knowledge and boost your career
    • Blackfire Profile and monitor performance of your apps
  • Other
  • Blog
  • Download
sponsored by SensioLabs
  1. Home
  2. Documentation
  3. Security
  4. How to Use Voters to Check User Permissions
  • Documentation
  • Book
  • Reference
  • Bundles
  • Cloud

Table of Contents

  • How Symfony Uses Voters
  • The Voter Interface
  • Setup: Checking for Access in a Controller
  • Creating the custom Voter
  • Configuring the Voter
  • Checking for Roles inside a Voter
  • Changing the Access Decision Strategy

How to Use Voters to Check User Permissions

Edit this page

Warning: You are browsing the documentation for Symfony 3.2, which is no longer maintained.

Read the updated version of this page for Symfony 6.2 (the current stable version).

How to Use Voters to Check User Permissions

In Symfony, you can check the permission to access data by using the ACL module, which is a bit overwhelming for many applications. A much easier solution is to work with custom voters, which are like simple conditional statements.

Tip

Take a look at the authorization article for an even deeper understanding on voters.

How Symfony Uses Voters

In order to use voters, you have to understand how Symfony works with them. All voters are called each time you use the isGranted() method on Symfony's authorization checker (i.e. the security.authorization_checker service). Each one decides if the current user should have access to some resource.

Ultimately, Symfony takes the responses from all voters and makes the final decision (to allow or deny access to the resource) according to the strategy defined in the application, which can be: affirmative, consensus or unanimous.

For more information take a look at the section about access decision managers.

The Voter Interface

A custom voter needs to implement VoterInterface or extend Voter, which makes creating a voter even easier.

1
2
3
4
5
abstract class Voter implements VoterInterface
{
    abstract protected function supports($attribute, $subject);
    abstract protected function voteOnAttribute($attribute, $subject, TokenInterface $token);
}

Setup: Checking for Access in a Controller

Suppose you have a Post object and you need to decide whether or not the current user can edit or view the object. In your controller, you'll check access with code like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// src/AppBundle/Controller/PostController.php
// ...

class PostController extends Controller
{
    /**
     * @Route("/posts/{id}", name="post_show")
     */
    public function showAction($id)
    {
        // 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 editAction($id)
    {
        // get a Post object - e.g. query for it
        $post = ...;

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

        // ...
    }
}

The denyAccessUnlessGranted() method (and also, the simpler isGranted() method) calls out to the "voter" system. Right now, no voters will vote on whether or not the user can "view" or "edit" a Post. But you can create your own voter that decides this using whatever logic you want.

Tip

The denyAccessUnlessGranted() and isGranted() functions are both just shortcuts of the Controller class to call isGranted() on the security.authorization_checker service. The main difference is that when access is not granted, denyAccessUnlessGranted() throws an AccessDeniedException, whereas isGranted() returns false.

Creating the custom Voter

Suppose the logic to decide if a user can "view" or "edit" a Post object is pretty complex. For example, a User can always edit or view a Post they created. And if a Post is marked as "public", anyone can view it. A voter for this situation would look like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
// src/AppBundle/Security/PostVoter.php
namespace AppBundle\Security;

use AppBundle\Entity\Post;
use AppBundle\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($attribute, $subject)
    {
        // if the attribute isn't one we support, return false
        if (!in_array($attribute, array(self::VIEW, self::EDIT))) {
            return false;
        }

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

        return true;
    }

    protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
    {
        $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;

        switch ($attribute) {
            case self::VIEW:
                return $this->canView($post, $user);
            case self::EDIT:
                return $this->canEdit($post, $user);
        }

        throw new \LogicException('This code should not be reached!');
    }

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

        // the Post object could have, for example, a method isPrivate()
        // that checks a boolean $private property
        return !$post->isPrivate();
    }

    private function canEdit(Post $post, User $user)
    {
        // this assumes that the data object has a getOwner() method
        // to get the entity of the user who owns this data object
        return $user === $post->getOwner();
    }
}

That's it! The voter is done! Next, configure it.

To recap, here's what's expected from the two abstract methods:

Voter::supports($attribute, $subject)
When isGranted() (or denyAccessUnlessGranted()) is called, the first argument is passed here as $attribute (e.g. ROLE_USER, edit) and the second argument (if any) is passed as $subject (e.g. null, a Post object). Your job is to determine if your voter should vote on the attribute/subject combination. If you return true, voteOnAttribute() will be called. Otherwise, your voter is done: some other voter should process this. In this example, you return true if the attribue is view or edit and if the object is a Post instance.
voteOnAttribute($attribute, $subject, TokenInterface $token)
If you return true from supports(), then this method is called. Your job is simple: return true to allow access and false to deny access. The $token can be used to find the current user object (if any). In this example, all of the complex business logic is included to determine access.

Configuring the Voter

To inject the voter into the security layer, you must declare it as a service and tag it with security.voter:

  • YAML
  • XML
  • PHP
1
2
3
4
5
6
7
8
# app/config/services.yml
services:
    app.post_voter:
        class:  AppBundle\Security\PostVoter
        tags:
            - { name: security.voter }
        # small performance boost
        public: false
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!-- app/config/services.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns="http://symfony.com/schema/dic/services"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://symfony.com/schema/dic/services
        http://symfony.com/schema/dic/services/services-1.0.xsd">

    <services>
        <service id="app.post_voter"
            class="AppBundle\Security\PostVoter"
            public="false"
        >

            <tag name="security.voter" />
        </service>
    </services>
</container>
1
2
3
4
5
6
7
// app/config/services.php
use AppBundle\Security\PostVoter;

$container->register('app.post_voter', PostVoter::class)
    ->setPublic(false)
    ->addTag('security.voter')
;

You're done! Now, when you call isGranted() with view/edit and a Post object, your voter will be executed and you can control access.

Checking for Roles inside a Voter

What if you want to call isGranted() from inside your voter - e.g. you want to see if the current user has ROLE_SUPER_ADMIN. That's possible by injecting the AccessDecisionManager into your voter. You can use this to, for example, always allow access to a user with ROLE_SUPER_ADMIN:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// src/AppBundle/Security/PostVoter.php

// ...
use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface;

class PostVoter extends Voter
{
    // ...

    private $decisionManager;

    public function __construct(AccessDecisionManagerInterface $decisionManager)
    {
        $this->decisionManager = $decisionManager;
    }

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

        // ROLE_SUPER_ADMIN can do anything! The power!
        if ($this->decisionManager->decide($token, array('ROLE_SUPER_ADMIN'))) {
            return true;
        }

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

Next, update services.yml to inject the security.access.decision_manager service:

  • YAML
  • XML
  • PHP
1
2
3
4
5
6
7
8
# app/config/services.yml
services:
    app.post_voter:
        class: AppBundle\Security\PostVoter
        arguments: ['@security.access.decision_manager']
        public: false
        tags:
            - { name: security.voter }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!-- app/config/services.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns="http://symfony.com/schema/dic/services"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://symfony.com/schema/dic/services
        http://symfony.com/schema/dic/services/services-1.0.xsd">

    <services>
        <service id="app.post_voter"
            class="AppBundle\Security\PostVoter"
            public="false"
        >
            <argument type="service" id="security.access.decision_manager"/>

            <tag name="security.voter" />
        </service>
    </services>
</container>
1
2
3
4
5
6
7
8
9
10
// app/config/services.php
use AppBundle\Security\PostVoter;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;

$container->register('app.post_voter', PostVoter::class)
    ->addArgument(new Reference('security.access.decision_manager'))
    ->setPublic(false)
    ->addTag('security.voter')
;

That's it! Calling decide() on the AccessDecisionManager is essentially the same as calling isGranted() from a controller or other places
(it's just a little lower-level, which is necessary for a voter).

Note

The security.access.decision_manager is private. This means you can't access it directly from a controller: you can only inject it into other services. That's ok: use security.authorization_checker instead in all cases except for voters.

Changing the Access Decision Strategy

Normally, only one voter will vote at any given time (the rest will "abstain", which means they return false from supports()). But in theory, you could make multiple voters vote for one action and object. For instance, suppose you have one voter that checks if the user is a member of the site and a second one that checks if the user is older than 18.

To handle these cases, the access decision manager uses an access decision strategy. You can configure this to suit your needs. There are three strategies available:

affirmative (default)
This grants access as soon as there is one voter granting access;
consensus
This grants access if there are more voters granting access than denying;
unanimous
This only grants access once all voters grant access.

In the above scenario, both voters should grant access in order to grant access to the user to read the post. In this case, the default strategy is no longer valid and unanimous should be used instead. You can set this in the security configuration:

  • YAML
  • XML
  • PHP
1
2
3
4
# app/config/security.yml
security:
    access_decision_manager:
        strategy: unanimous
1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- app/config/security.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<srv:container xmlns="http://symfony.com/schema/dic/security"
    xmlns:srv="http://symfony.com/schema/dic/services"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://symfony.com/schema/dic/services
        http://symfony.com/schema/dic/services/services-1.0.xsd"
>

    <config>
        <access-decision-manager strategy="unanimous" />
    </config>
</srv:container>
1
2
3
4
5
6
// app/config/security.php
$container->loadFromExtension('security', array(
    'access_decision_manager' => array(
        'strategy' => 'unanimous',
    ),
));
This work, including the code samples, is licensed under a Creative Commons BY-SA 3.0 license.
TOC
    Version
    We stand with Ukraine.
    Version:
    Symfony Code Performance Profiling

    Symfony Code Performance Profiling

    Peruse our complete Symfony & PHP solutions catalog for your web development needs.

    Peruse our complete Symfony & PHP solutions catalog for your web development needs.

    Symfony footer

    ↓ Our footer now uses the colors of the Ukrainian flag because Symfony stands with the people of Ukraine.

    Avatar of Adam Wójs, a Symfony contributor

    Thanks Adam Wójs (@awojs) for being a Symfony contributor

    3 commits • 76 lines changed

    View all contributors that help us make Symfony

    Become a Symfony contributor

    Be an active part of the community and contribute ideas, code and bug fixes. Both experts and newcomers are welcome.

    Learn how to contribute

    Symfony™ is a trademark of Symfony SAS. All rights reserved.

    • What is Symfony?

      • Symfony at a Glance
      • Symfony Components
      • Case Studies
      • Symfony Releases
      • Security Policy
      • Logo & Screenshots
      • Trademark & Licenses
      • symfony1 Legacy
    • Learn Symfony

      • Symfony Docs
      • Symfony Book
      • Reference
      • Bundles
      • Best Practices
      • Training
      • eLearning Platform
      • Certification
    • Screencasts

      • Learn Symfony
      • Learn PHP
      • Learn JavaScript
      • Learn Drupal
      • Learn RESTful APIs
    • Community

      • SymfonyConnect
      • Support
      • How to be Involved
      • Code of Conduct
      • Events & Meetups
      • Projects using Symfony
      • Downloads Stats
      • Contributors
      • Backers
    • Blog

      • Events & Meetups
      • A week of symfony
      • Case studies
      • Cloud
      • Community
      • Conferences
      • Diversity
      • Documentation
      • Living on the edge
      • Releases
      • Security Advisories
      • SymfonyInsight
      • Twig
      • SensioLabs
    • Services

      • SensioLabs services
      • Train developers
      • Manage your project quality
      • Improve your project performance
      • Host Symfony projects

      Deployed on

    Follow Symfony

    Search by Algolia