Buy
Buy

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

Time to create our new Voter class! To do it... we can cheat! Find your terminal and run:

php bin/console make:voter

Call it ArticleVoter. It's pretty common to have one voter per object that you need to decide access for. Let's go check it out src/Security/Voter/ArticleVoter.php:

... lines 1 - 2
namespace App\Security\Voter;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
use Symfony\Component\Security\Core\User\UserInterface;
class ArticleVoter extends Voter
{
protected function supports($attribute, $subject)
{
// replace with your own logic
// https://symfony.com/doc/current/security/voters.html
return in_array($attribute, ['EDIT', 'VIEW'])
&& $subject instanceof App\Entity\BlogPost;
}
protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
{
$user = $token->getUser();
// if the user is anonymous, do not grant access
if (!$user instanceof UserInterface) {
return false;
}
// ... (check conditions and return true to grant permission) ...
switch ($attribute) {
case 'EDIT':
// logic to determine if the user can EDIT
// return true or false
break;
case 'VIEW':
// logic to determine if the user can VIEW
// return true or false
break;
}
return false;
}
}

supports()

Nice! Voters are a bit simpler than authenticators: just two methods. Here's how it works: whenever anybody in the system calls isGranted() with any permission attribute string, the supports() method on your voter will be called:

... lines 1 - 8
class ArticleVoter extends Voter
{
protected function supports($attribute, $subject)
{
// replace with your own logic
// https://symfony.com/doc/current/security/voters.html
return in_array($attribute, ['EDIT', 'VIEW'])
&& $subject instanceof App\Entity\BlogPost;
}
... lines 18 - 40
}

It's our job to decide whether or not our voter knows how to vote.

The $attribute argument will be the string passed to isGranted() and $subject is the second argument - the Article object for us. The example in the generated code is actually pretty good. Let's say that our voter knows how to vote if the $attribute is MANAGE and if the $subject is an instanceOf Article:

... lines 1 - 4
use App\Entity\Article;
... lines 6 - 9
class ArticleVoter extends Voter
{
protected function supports($attribute, $subject)
{
// replace with your own logic
// https://symfony.com/doc/current/security/voters.html
return in_array($attribute, ['MANAGE'])
&& $subject instanceof Article;
}
... lines 19 - 41
}

If we return false from supports, nothing happens: Our ArticleVoter doesn't vote and it's up to some other voter to handle things. But if we return true, Symfony immediately calls voteOnAttribute():

... lines 1 - 8
class ArticleVoter extends Voter
{
... lines 11 - 18
protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
{
$user = $token->getUser();
// if the user is anonymous, do not grant access
if (!$user instanceof UserInterface) {
return false;
}
// ... (check conditions and return true to grant permission) ...
switch ($attribute) {
case 'EDIT':
// logic to determine if the user can EDIT
// return true or false
break;
case 'VIEW':
// logic to determine if the user can VIEW
// return true or false
break;
}
return false;
}
}

This is where our logic goes to determine access. If we return true, access will be granted. If we return false, access will be denied.

voteOnAttribute()

Symfony passes us the same $attribute and $subject, as well as something called the $token:

... lines 1 - 4
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
... lines 6 - 8
class ArticleVoter extends Voter
{
... lines 11 - 18
protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
{
$user = $token->getUser();
... lines 22 - 39
}
}

The token is a lower-level object that you don't see too often. But, you can use it to get access to the User object:

... lines 1 - 4
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
... line 6
use Symfony\Component\Security\Core\User\UserInterface;
class ArticleVoter extends Voter
{
... lines 11 - 18
protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
{
$user = $token->getUser();
// if the user is anonymous, do not grant access
if (!$user instanceof UserInterface) {
return false;
}
... lines 26 - 39
}
}

I'm going to start in this method by helping my editor. At the top, add /** @var Article $subject */ to say that the $subject variable is an Article object:

... lines 1 - 9
class ArticleVoter extends Voter
{
... lines 12 - 19
protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
{
/** @var Article $subject */
... lines 23 - 40
}
}

We can safely do this because of the supports() method:

... lines 1 - 9
class ArticleVoter extends Voter
{
protected function supports($attribute, $subject)
{
// replace with your own logic
// https://symfony.com/doc/current/security/voters.html
return in_array($attribute, ['MANAGE'])
&& $subject instanceof Article;
}
... lines 19 - 41
}

$subject will definitely be an Article at this point.

Below this, it's pretty common to have a voter that votes on multiple attributes, like EDIT and DELETE. We don't need it, but I'll keep the switch case statement. Our only case is MANAGE:

... lines 1 - 9
class ArticleVoter extends Voter
{
... lines 12 - 19
protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
{
/** @var Article $subject */
$user = $token->getUser();
// if the user is anonymous, do not grant access
if (!$user instanceof UserInterface) {
return false;
}
// ... (check conditions and return true to grant permission) ...
switch ($attribute) {
case 'MANAGE':
... lines 32 - 36
break;
}
... lines 39 - 40
}
}

Excellent! It's time to shine. First, if $subject->getAuthor() == $user then return true:

... lines 1 - 9
class ArticleVoter extends Voter
{
... lines 12 - 19
protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
{
... lines 22 - 28
// ... (check conditions and return true to grant permission) ...
switch ($attribute) {
case 'MANAGE':
// this is the author!
if ($subject->getAuthor() == $user) {
return true;
}
break;
}
return false;
}
}

The current user is the author and so access should be granted.

Checking for Roles inside a Voter

If they are not the author, we need to check for ROLE_ADMIN_ARTICLE. But, hmm. We know how to check if a User has a role in a controller: $this->isGranted():

... lines 1 - 11
class ArticleAdminController extends AbstractController
{
... lines 14 - 31
public function edit(Article $article)
{
if (!$this->isGranted('MANAGE', $article)) {
... line 35
}
... lines 37 - 38
}
}

But, how can we check that from inside of a voter? Or, from inside any service?

The answer is.... with the Security service! We actually already know this service! Add a public function __construct() method with a new Security argument: the one from the Symfony component. I'll hit Alt+Enter and select "Initialize Fields" to create that property and set it:

... lines 1 - 7
use Symfony\Component\Security\Core\Security;
... lines 9 - 10
class ArticleVoter extends Voter
{
private $security;
public function __construct(Security $security)
{
$this->security = $security;
}
... lines 19 - 53
}

Do you remember where we used this service before? It was inside MarkdownHelper: it's the last argument way over here:

... lines 1 - 9
class MarkdownHelper
{
... lines 12 - 16
private $security;
public function __construct(AdapterInterface $cache, MarkdownInterface $markdown, LoggerInterface $markdownLogger, bool $isDebug, Security $security)
{
... lines 21 - 24
$this->security = $security;
}
... lines 27 - 48
}

We used it because it gives us access to the current User object:

... lines 1 - 9
class MarkdownHelper
{
... lines 12 - 27
public function parse(string $source): string
{
if (stripos($source, 'bacon') !== false) {
$this->logger->info('They are talking about bacon again!', [
'user' => $this->security->getUser()
]);
}
... lines 35 - 47
}
}

But, there's one other thing that the Security class can do. Hold Command or Ctrl and click to open it. It has a getUser() method but it also has an isGranted() method! Awesome! The Security service is the key to get the User or check if the user has access for some permission attribute.

Back down in our voter logic, it's now very simple: if $this->security->isGranted('ROLE_ADMIN_ARTICLE'), then return true. At the bottom, instead of break, return false: if both of these conditions are not met, access denied:

... lines 1 - 10
class ArticleVoter extends Voter
{
... lines 13 - 27
protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
{
... lines 30 - 36
// ... (check conditions and return true to grant permission) ...
switch ($attribute) {
case 'MANAGE':
// this is the author!
if ($subject->getAuthor() == $user) {
return true;
}
if ($this->security->isGranted('ROLE_ADMIN_ARTICLE')) {
return true;
}
return false;
}
return false;
}
}

Ok, let's try this! Move over, refresh and... access granted! Symfony calls the supports() method, that returns true, and because we're logged in as the author, access is granted. Comment out the author check real quick:

// src/Security/Voter/ArticleVoter.php

class ArticleVoter extends Voter
{
    // ...
    protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
    {
        switch ($attribute) {
            case 'MANAGE':
                // this is the author!
                if ($subject->getAuthor() == $user) {
                    //return true;
                }
                // ...
        }

        return false;
    }
}

Try it again. Access denied! Put that back.

@IsGranted with a Subject

Voters are great. And using them to centralize this kind of logic will keep your security code solid. But, there's one small thing that now seems impossible to do. First, open ArticleAdminController. We can actually shorten this to the normal $this->denyAccessUnlessGranted('MANAGE', $article):

... lines 1 - 11
class ArticleAdminController extends AbstractController
{
... lines 14 - 31
public function edit(Article $article)
{
$this->denyAccessUnlessGranted('MANAGE', $article);
dd($article);
}
}

Try it - reload the page. Access granted! This does the exact same thing as before. But... what about using the @IsGranted() annotation?

... lines 1 - 6
use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted;
... lines 8 - 11
class ArticleAdminController extends AbstractController
{
/**
... line 15
* @IsGranted("ROLE_ADMIN_ARTICLE")
*/
public function new(EntityManagerInterface $em)
{
... lines 20 - 26
}
... lines 28 - 37
}

Hmm... now there's a problem: can we use the annotation and still, somehow, pass in the Article object? Actually, yes!

Add @IsGranted(), pass it MANAGE and then a second argument: subject="article":

... lines 1 - 11
class ArticleAdminController extends AbstractController
{
... lines 14 - 28
/**
... line 30
* @IsGranted("MANAGE", subject="article")
*/
public function edit(Article $article)
{
... line 35
}
}

That's it! When you use subject=, you're allowed to pass this the same name as any of the arguments to your controller. This only works because we used the feature that automatically queries for the Article object and passes it as an argument. These two features combine perfectly. But, if you're ever in a situation where your "subject" isn't a controller argument, no worries, just use the normal denyAccessUnlessGranted() code. But, remove it in this case:

... lines 1 - 11
class ArticleAdminController extends AbstractController
{
... lines 14 - 28
/**
... line 30
* @IsGranted("MANAGE", subject="article")
*/
public function edit(Article $article)
{
dd($article);
}
}

Let's... try it! Access granted! That was too easy. Go back to the voter and comment-out the author check again - let's really make sure this is working:

// src/Security/Voter/ArticleVoter.php

class ArticleVoter extends Voter
{
    // ...
    protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
    {
        switch ($attribute) {
            case 'MANAGE':
                // this is the author!
                if ($subject->getAuthor() == $user) {
                    //return true;
                }
                // ...
        }

        return false;
    }
}

Now... yes! Access denied! Go put that code back.

Oh my gosh friends, we did it! We killed this tutorial! We have a great authentication system that allows both login form authentication and API authentication! We have a rich dynamic roles system and a voter system where we can control access with any custom rules. Oh, I love security! I hope you guys are feeling empowered to create your simple, complex, crazy, whatever authentication system you need. As always, if you have questions, ask us down in the comments.

Alright people, seeya next time!

Leave a comment!

  • 2019-02-25 weaverryan

    It still lives in an external package - https://github.com/symfony/... - but I still don't recommend it :)

  • 2019-02-22 cybernet2u

    ACL support was removed in Symfony 4.0

  • 2019-01-29 Vladimir Sadicov

    It's good solution! Hope everything will work as expected!

    Cheers!

  • 2019-01-28 Diego Aguiar

    Oh, I get it now. It deppends on how you want to maintain/scale this functionality but if the voting logic of CREATE/LIST actions is not so different from EDIT/DELETE, then I would put it all in the same Voter. But, if you need different services for such logic, then I would prefer to split into two classes.

    Cheers!

  • 2019-01-25 qenix

    I have 3rd party desktop software but I know how password was hashed so I maped table to doctrine enitity and created CustomAuthenticator. :)

  • 2019-01-25 elbarto

    Hey Diego, i think i didnt explain my problem well enough.

    Here's an example with an Article Entity, ArticleController and its ArticleVoter. What i would like, is to be able to manage the authorization of all my ArticleController's actions (A basic CRUD : CREATE, LIST, EDIT, DELETE) in the ArticleVoter.

    However, the ArticleVoter's support() method is expecting an $attribute and a $subject. So for an EDIT or DELETE action, this is working very well because i can pass the Article to edit/delete as the $subject (as expected).

    Now, if i want to add permissions to the CREATE action, there's a problem because it expects an Article $subject and since its a creation, i do not have any Article to provide as a $subject, so the support method will return false.




    return in_array($attribute, ['CREATE', 'EDIT', 'DELETE', 'LIST'])


    && $subject instanceof Article;


    So my question is how would you deal with that ?

    Update the names to ARTICLE_CREATE, ARTICLE_EDIT, ARTICLE_DELETE, ARTICLE_LIST and add an additional simple check like so if we can't attach an Article object :




    if(!$subject) // no subject, so we just check if the $attribute is in another array of permissions



    return in_array($attribute, ['ARTICLE_CREATE', 'ARTICLE_LIST'])


    This way we got everything in the same Voter !

    Or would you rather keep it separated and deal with the CREATE / LIST actions by adding them in the security.yaml under the right roles in roles_hierarchy ?


    role_hierarchy:
    ROLE_ADMIN:
    - ROLE_ARTICLE_CREATE
    - ROLE_ARTICLE_LIST

    Thanks !

  • 2019-01-24 Diego Aguiar

    Hey @elbarto

    Why you don't have access to a user object? You can pass the logged in user as the subject or maybe the user's id comes from the request? If someone is trying to delete a record, then he/she should be logged in at least, isn't it? or maybe I'm missing something obvious

    Cheers!

  • 2019-01-24 elbarto

    hi there, as always, thanks for the amazing content. I had some thoughts about voters. Right now, the way i am handling authorization is as follow (for a User Entity) : If i only need to check for a ROLE, i'll create a specific name such as ROLE_CREATE_USER for instance and add it in the security.yaml file under roles_hierarchy to all my 'MAIN' roles such as ROLE_ADMIN, ROLE_EDITOR etc.. . If i have to check for more than just a simple ROLE i would go with a voter passing it the related entity / object to check additional stuff (for instance that the person who wants to edit a user must be over 18 years old), but this does not work for every case. So my question is : If i wanted to move my "CREATE_USER" or even a "DELETE_USER" permission in a UserVoter, how could i do that, i mean in my controller i do not have a user object to pass as a subject ! I'd like to centralize everything in my voter (such as CREATE, LIST, EDIT, DELETE).

  • 2019-01-22 Vladimir Sadicov

    hey qenix

    May I ask what will be your next step? are you going to maintain your old login system? or migrate it to something new? Anyways I think that mapping it to Doctrine entity is a good thing, and everything else depends on your future plans. You can create Custom guard authenticator, or maybe use standard symfony form_login system, you just need to set correct password hash algorithm to security.yaml file.

    Cheers!

  • 2019-01-21 qenix

    Diego Aguiar Thanks for response.I would like to go another way.

    It's a good point of view? -> to map old system login database table to doctrine entity.

    And then create CustomAuthenticator which checks and in getUser put all logic?

  • 2019-01-17 Diego Aguiar

    Hey qenix

    Probably this documentation may help you out understanding what you need to do: https://symfony.com/doc/cur...

    Cheers!

  • 2019-01-16 qenix

    Hi Ryan, Thanks for powerful course.

    I would like to ask how can I integrate Symfony Login system with (old php login system)*. I need Symfony login provider that use old login system logic and query for user to old database in MSSQL. What I need to do, that I have really old system but I would like use old logins and password from old system in new project with Symfony and grand access if authentication success. Could You suggest solution?

  • 2019-01-14 Diego Aguiar

    Hey caprarolainfo

    Thanks for your kind words ;)

    What you did will work but it doesn't mean that our code is good or bad, it just depends on the logic. In our case we allow to edit articles only if you are the author or if you have a specific role for editing articles. But look like your logic is different, you only want to allow authors to edit their own articles, which it's ok as well, all of it depends on your "security rules".

    Cheers!

  • 2019-01-14 caprarolainfo

    Hi Ryan, congratulations for your work. I'm writing the code while I'm studying your tutorials and I've found a problem. If an admin user tries to change another admin user's article, I do not receive any errors!
    In fact in ArticleVoter in the first check

    if ($ subject-> getAuthor () == $ user)

    it does not enter but in the second

    if ($ this-> security-> isGranted ('ROLE_ADMIN_ARTICLE'))

    being an administrator enters.

    I modified the first check in
    if ($ subject-> getAuthor ()! = $ user) {
    return false;
    }
    and now it works.

    I do not know if I wrote a fool, if that were not to take into consideration what I wrote. If it is correct, you must be happy, because all I know about Symfony I learned it from you!
    thank you
    Marco

  • 2018-12-24 Victor Bocharsky

    Hey Ivan,

    Symfony has a complex solution called Access Control Lists (ACLs). But I bet you don't need so much complexity, so I'd recommend you to look at Voters first - they are much simple but flexible and you can easily do what you want. We also have a few screencasts about voters, you can use our search to find more places where we mention them: https://symfonycasts.com/se... - or see this latest screencasts for Symfony 4 where we talk about Voters: https://symfonycasts.com/sc...

    Cheers!

  • 2018-12-23 Ivan

    Hi, we are going to build some complex permission architecture with permission tree.
    And need advice is there best practices in Symfony to realize something like this:

    We have users:

    1 - Role: Boss, Name: Boss
    2 - Role: Manager, Name: Manager-1
    3 - Role: Manager, Name: Manager-2
    4 - Role: HireManager, Name: HireManager-1

    The boss should manage permission by modules:

    1 - Allow to Role "Manager" use module - "Orders"
    2 - For manager "Manager-2" (personal permission) in module "Orders" hide "Client data" (a couple of fields)
    4 - HireManager hasn't access to module "Orders"
    3 - If in future Boss will add new role "SeniorManager", and give him permissions to module "Orders", all users with role "SeniorManager" should have access to module "Orders" without any changes in code.

  • 2018-11-26 Victor Bocharsky

    Hey Steve,

    Haha, good feeling! I had to try it ;)

    Cheers!

  • 2018-11-26 Steve

    Hi Victor, thank you for this, I had a sneaky feeling that was going to be the answer and should work perfectly.

  • 2018-11-26 Victor Bocharsky

    Hey Steve,

    Sure, why not? Just use "is_granted()" Twig function and pass to it the attribute and the object, something like this:


    {% if is_granted('edit', post) %}
    {# show edit link here... #}
    {% endif %}

    Cheers!

  • 2018-11-24 Steve

    Hi,

    Great video and voters is something I could really use but can voters be used to show/hide elements in twig templates.

    I may want to allow a user to see all posts but not the option to edit it. I suppose in the Spacebar example this would come in to play for comments. Everyone can see the comments but only the comment author can access the edit screen, can voters be used to hide the edit button/link.

    Cheers

    Steve

  • 2018-10-29 weaverryan

    Hey Peter Kosak!

    Good timing on this :). I was just "finishing" my video recording last week for forms... then decided, yea, I really should talk about form events & dependent form fields. Then I saw your message! So, we're on the same page. I will cover it - at the end of the tutorial. Thanks for the feedback!

    Cheers!

  • 2018-10-26 Peter Kosak

    Thanks Ryan and whole knp team for this course. Next one is probably "Form" course cant wait. I would just suggest example 2 & mainly 3 from here https://symfony.com/doc/cur...

    There is not a lot of exampled on the internet regarding above topic and it is widely used over the internet.