How to Use Voters to Check User Permissions
Warning: You are browsing the documentation for Symfony 2.x, which is no longer maintained.
Read the updated version of this page for Symfony 7.1 (the current stable version).
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);
}
2.8
The Voter
helper class was added in Symfony 2.8. In earlier versions, an
AbstractVoter
class with similar behavior was available.
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()
(ordenyAccessUnlessGranted()
) 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
, aPost
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 returntrue
if the attribute isview
oredit
and if the object is aPost
instance. voteOnAttribute($attribute, $subject, TokenInterface $token)
-
If you return
true
fromsupports()
, then this method is called. Your job is simple: returntrue
to allow access andfalse
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
:
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
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
. The isGranted()
method
is provided by the security.authorization_checker
service via the
AccessDecisionManager
class. However, you can't inject that service because it causes a
CircularReferenceException
.
The solution is to inject the service_container
service and use it to get
the security.authorization_checker
service and call to the isGranted()
method.
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 if there is no voter denying access. If all voters
abstained from voting, the decision is based on the
allow_if_all_abstain
config option (which defaults tofalse
).
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:
1 2 3 4 5
# app/config/security.yml
security:
access_decision_manager:
strategy: unanimous
allow_if_all_abstain: false