Adding a Custom Voter

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $12.00

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!

  • 2020-06-24 weaverryan

    Hey Travel RN Hub!

    Excellent question :). The answer depends on your requirements. Here are some questions:

    A) In the database, are these 3 different types of users stored in different database tables? Or just one, and each has different data or permissions?
    B) Do doctors, EMS and nurses ever use the same parts of the site? Or is it almost like there are 3 different sites and there is no cross-over

    In general, you have 2 options:

    1) You basically just have 1 User table, which contains all the users. On the outside, it feels like 3 different users, but it's really the same list, with maybe a flag on the user that says the user type. For this system, I would have 1 firewall, but 3 login forms with 3 authenticators. Design each authenticator to only allow login from the correct type.

    2) You basically have 3 different user tables. Depending in if there is ANY overlap of parts of the site that they use, I would still use 1 firewall or possibly 3 (if it is almost like there are 3 different sites). After that, the solution is kind of the same as (1): create 3 login forms and 3 authenticators. Each authenticator processes its 1 login form and loads users of that one type.

    Let me know if that helps! You may also be asking more about the "authorization" side of things. To answer that, let me know what the differences are (from a data perspective) for these 3 users. 3 different tables? 1 table with a "userType" property?

    Cheers!

  • 2020-06-23 Travel RN Hub

    How do you go about Multi-Auth system where you have 3 different login forms. for example one for doctors, one for EMS, one for Nurses? thank you

  • 2020-04-13 Victor Bocharsky

    Hey John,

    Good catch on that support(). So yes, I think you can just think of a slightly different logic in that support, e.g. check if the $subject is instance of Comment entity OR Comment::class string is matching the given string.

    Another option, just create an empty object. entities are just simple data objects without any dependency injection, so it should be pretty lightweight thing to do. But yeah, probably the first option is more elegant.

    I hope this helps!

    Cheers!

  • 2020-04-10 John

    In the voter we can have our logic for each attribute (edit, delete...) but it seems impossible to design an attribute "CREATE" or "NEW" (have a logic of who can create this type of object).
    Because with is_granted (twig template and php controller) we must specify the object.

    Example, for an existing Comment entity, we can have EDIT (also DELETE...) attribute and we can write :

    {% if is_granted('EDIT', comment) %}


        
    /**
    * @Route(".../{id}", name="comment_edit")
    * @IsGranted("EDIT", subject="comment")
    */
    public function edit(Comment $comment)

    So in both cases the object exists, it works.

    But, in a standard template (without controller data passed) if I want an 'CREATE' attribute (with my logic) and only display a user form for post a comment, I can't use :

    {% if is_granted('CREATE', 'App\Entity\Comment') %}

    Because the second argument must be an object, not FQCN. Same for Is_granted in a php annotation.

    Voter :


    protected function supports($attribute, $subject)
    {
    return in_array($attribute, ['CREATE', 'EDIT', 'DELETE'])
    && $subject instanceof Comment;
    }

    protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token)
    {
    $comment = $subject;

    switch ($attribute) {
    case 'CREATE':
    return true;
    }

    return false;
    }

    Do you understand what I'm saying ?

    So my question is, do you have a trick to overcome this in a Voter or we only can use a service/twig extension.

    While writing this answer I found that it comes from the instance of in supports(), if you remove it it works.
    Except that it will apply globally in the app (but we can have a different logic for an entity A and an entity B). We should find an alternative to the FQCN.

    Thanks !

  • 2020-03-26 Victor Bocharsky

    Hey John,

    Do you want to use the same template for both new and edit action? If so, you can check for entity ID. If it's set - then we're talking about existent entity in the DB... but if it's null - then you have a new entity that was not saved into the DB yet. Does it help?

    Cheers!

  • 2020-03-25 John

    Hello,

    just one question. How you go about not duplicating the logic between twig and the controller for a "new" object.
    In other words, some sort of "CREATE" attribute for an entity where the object not yet created.

    By adding a voter with "create" attribute, I have tried that but it doesn't work :
    twig : {% if is_granted('CREATE', 'App\Entity Thread') %}
    php : @IsGranted("CREATE", subject="App\Entity\Thread")

    Twig code is not displayed / and for php annotation : could not find the subject "App\Entity\Thread" for the @IsGranted annotation. Try adding a "$App\Entity\Thread" argument to your controller method.

    If there is no solution, would you have a recommendation? That seems to be a question that many people wonder!

    Thanks !

  • 2020-03-13 Victor Bocharsky

    Hey Lydie,

    Yes, exactly, it won't take into account role hierarchy. This Symfony Security system works with current (authenticated) user because it's complex and designed so. If you really need to take into account role hierarchy - look at RoleHierarchy service that already has some logic to work with it, so you probably don't need to "invent the wheel" for this business logic. This thread on StackOverflow may help you I think: https://stackoverflow.com/q...

    Cheers!

  • 2020-03-12 Lydie

    Hey Victor,

    I thought about this solution too but this does not take into account the hierarchy we can have for role.

    Cheers!

  • 2020-03-09 Victor Bocharsky

    Hey Lydie,

    You can just add a User::hasRole() method to be able to perform this check, e.g. look at such implementation from FOSUserBundle: https://github.com/FriendsO... - as soon as you have a User object - you can call that method on it. Does it help? :)

    Cheers!

  • 2020-03-04 Lydie

    Hello!

    How can I check that a user has a specific role? Not the user that is currently logged in. Let me explain. A logged in user can assign tickets to other users. If the assigned user has a specific role, it will received a notification mail. There is no hasRole() function for user. I thought voters could help but except if I am wrong, it's not the case. Any suggestion?

    Thx!

  • 2020-02-25 Rob

    Yep I am thinking all ALL registered users will be able to do, so I will check directly for the ROLE_USER only; that makes sense. Thanks Diego!

  • 2020-02-24 Diego Aguiar

    Hey Rob

    Is that a thing that ALL user will be able to do? If that's the case, in my opinion, the role ROLE_ADMIN_ARTICLE should not even exist. You should be checking directly for the ROLE_USER instead, but anyways, what you did is totally valid

    Cheers!

  • 2020-02-24 Rob

    I have a question related to role_hierarchy.

    If I wanted to grant all users that have the role: ROLE_USER to be granted same role as ROLE_ADMIN_ARTICLE, is it as simple as adding ROLE_USER to the role_hierarchy as below:

    role_hierarchy:
    ROLE_ADMIN: [ROLE_ADMIN_COMMENT, ROLE_ADMIN_ARTICLE, ROLE_ALLOWED_TO_SWITCH]
    ROLE_USER: [ROLE_ADMIN_ARTICLE]

    Then in the Controller:

    /**
    * @Route("/admin/article/new", name="app_article_new")
    * @IsGranted("ROLE_ADMIN_ARTICLE")
    */
    public function new(EntityManagerInterface $em, Request $request)
    { .... }

    It works perfect but thought I would ask if this is a good approach. I'm working on a app where I want to allow registered and logged in users with the role ROLE_USER to be allowed to create and edit new objects. Thanks for taking a look!

  • 2019-12-05 Dung Le

    In my mind, I always think security is a mess in programming but Symfony framework surely organized it so that I can understand and use security service of Symfony, great tutorial, thanks all!

  • 2019-11-12 weaverryan

    Hey Annemieke Buijs!

    Phew! An excellent question! First:

    > Hi all, I love voters

    Me too :).

    Ok, so you've stumbled on an annoying part of "authorization" in general. *Usually* you are running around asking "does the current user have access to do YYY with the ZZZ object?". As you mentioned, you're always asking about whether or not the *current* user has access to something. Now suddenly, you need to do something that seems similar: load some "data" that the user is allowed to access.

    Let's use a simple example. Imagine you have a voter that determines whether or not a User have access to Edit a specific Product. The logic for how you determine whether or not the user has "Edit" access aren't important - but maybe you check to see if they are an "admin" (they can see everything) and also if the created the product... and maybe also if they are part of some "Organization" that owns the product (just to invent some more conditions).

    Writing voters for all of this is easy. But now someone asks you: can you create a list of all of the products that a certain user has access to edit? How can we do that? The answer is actually probably *not* voters. Think about it: no matter how well you organize your code, you can't query for *every* product in the database (what if we have 100,000?) just to put each through a voter and determine the 10 that the user has access to. No, when you need to "list" some products... and that list is based on security... it (unfortunately) needs to be solved in a different way. The truth is that, for this example, you will need to write a query that returns "all the products I created OR all the products that are owned by an organization that I am a member of". The logic to build that query will be SO similar to the logic in your voters... but they probably won't *actually* overlap and be re-usable. So, you probably *will* have some duplication here - it's just the nature of deciding access on one object vs querying for a list of objects from a database (or ldap).

    Now, to your situation :). A few points:

    A) based on my long-winded answer above, what you need to do is determine a way to "query" active directory to get the user list, instead of looping over every user and asking if each has access to a certain url/application.
    B) Unless... your total user list is quite small. Then... yea... in theory, you *could* loop over all of the users and re-use your voter logic. To do that, the best thing to do would be to isolate your voter logic to a separate service class and then have both your voter and your custom code for this situation call that. In that service class might have a function like voteOnAttribute(string $attribute, User $user) (where the User type-hint is YOUR user class), just as an example. You can probably see how a voter could get the User from the token, then call this method on that other service. And your code could easily fetch the User and call the same method.

    Phew! Let me know if this helps!

    Cheers!

  • 2019-11-08 Annemieke Buijs

    Hi all, I love voters. As soon as i started working with symfony i used voters.
    But right now i have a challenge.
    In the 'admin' panel I want to show a list of users that have access to a certain url/application.

    Since i put a lot of security in voters and not so mutch in roles in the user provider i wonder how i can create this list of users.
    An example of what i do in a voter is check if user is a manager in ldap/activedirectory.

    As far as i now, a voter can only be uses on the current (loggedin user) and not on a random user to check wether this user has access to url.
    Can anyone help me please?

  • 2019-11-01 Alexandr Kapustin

    Yeah. I actually come up with the same thoughs :D
    Thank you very much!

  • 2019-10-27 weaverryan

    Hey @Alexandr!

    Woo! Awesome :). Last bit about dynamic roles for a controller... would mostly mean *another* database table for those. For example, you might secure a controller via isGranted() by its name - eg ArticleController::foo(). But in your voter, you would then look that value up in some database table, which would map to the *actual* role or roles to check for. Then you could tweak the roles needed for each controller in the database. It would be kinda crazy... but totally work ;).

    Cheers!

  • 2019-10-23 Alexandr Kapustin

    Cool!
    Sorry for late reply, I've just noticed your answer :(

    Thank you very much weaverryan for your detailed answer! I will try it!


    If you even need the role that you use to protect a controller to be dynamic, let me know


    Described method already helps a lot! Thank you! But just my curiosity, it would be interesting to know :) Maybe one day in some tutorial ? :)

    PS: your courses are amazing!

  • 2019-10-20 weaverryan

    Hey Alexandr Kapustin!

    Cool question. I don't think you're missing anything - you just have a more complex setup. And so a few of the "normal" things just won't work for you (like the static "roles" in config).

    Here's what I would probably do. First, "roles" currently have a problem with Symfony (I have an unfinished PR to fix this) that if you dynamically (e.g. through some admin interface) remove/add a role to a user, that won't take effect until the user logs out and logs back in. So, for your situation, let's just not use roles. And... that's fine! Roles are nothing more than a "shortcut" - there is one built-in voter that looks simply looks at what role a controller requires (e.g. ROLE_USER) and then checks to see if the User has that role. It's very simple, so we're not losing much by just writing our own.

    Instead, imagine a setup like this. I'm going to continue to use the word "role", but we will be creating our own role-checking system:

    A) In the database, set setup whatever structure you want for allowing users to have dynamic roles. Maybe you have a User.roles property... or relationship between User & Roles - whatever you want. This just comes down to database design and has nothing to do with security.

    B) To secure a specific controller, you have two options. The simpler one is to just secure it with a specific role. Not: we will not sure ROLE_ as a role prefix anymore, but need to use something else so that the core "RoleVoter" doesn't try to make the decision for us. So, for example, suppose there is some "blog post admin" area - you might secure the "new" action with ACCESS_ROLE_BLOG_CREATE. If you even need the role that you use to protect a controller to be dynamic, let me know. You can do that two (that would be the second way to do things), it just adds another layer.

    C) Finally, you implement a single voter - e.g. AccessRoleVoter - that votes on everything that starts with (in this example) ACCESS_ROLE_. Very simply, you look at the "role" being checked (e.g. ACCESS_ROLE_BLOG_CREATE) then use *whatever* logic you want to determine whether or not the current user has this role. This logic could be *anything*.

    So, that's it! Does this help? Is it still missing some pieces. Let me know!

    Cheers!

  • 2019-10-20 Alexandr Kapustin

    Hello all!
    After viewing this course I'm a bit confused.
    So if I want to create complex ACL system, with storing list of Roles in database. Also I want to have list of permissions which I can enable or disable by setting flag into database so I can manage every role dynamically ? Ah, and of course, there is also should be relation (oneToMany or ManyToMany) between roles and users.

    Defining ROLES in config file doesn't really look great, because it's static and no flexibility here.

    I can not see a simple way to do that. In this case for sure we should not use native firewalls (except the case where do we need to check is user logged in or not).
    I'm not even sure that we should use Voters here.
    Did I miss something here ?

  • 2019-09-16 Vladimir Sadicov

    This is really good question. =)

    Ok now I see what's the difference. Where did you get provider: db_user_provider row in your main firewall? if you remove it, then everything should work as expected.

    Cheers!

  • 2019-09-13 adam

    then another error occured The service "security.firewall.map" has a dependency on a non-existent service ".security.request_matcher.Iy.T22O".

    i don't know why they suggest that in the error but in https://symfony.com/doc/cur... they are setting it inside guard not at the same level

  • 2019-09-13 Vladimir Sadicov

    Whoops this one will not help =( Try to move entry_point: at the same level as guard: as mentioned in error!

    Cheers

  • 2019-09-12 adam

    security:
    # https://symfony.com/doc/cur...
    providers:
    db_user_provider:
    entity:
    {
    class: App\Entity\User,
    property: email
    }
    encoders:
    App\Entity\User:
    algorithm: auto
    firewalls:
    dev:
    pattern: ^/(_(profiler|wdt)|css|images|js)/
    security: false
    main:
    anonymous: ~
    pattern: ^/
    http_basic: ~
    provider: db_user_provider
    form_login:
    login_path: app_login
    check_path: app_login
    default_target_path: /
    logout:
    path: app_logout
    target: /
    remember_me:
    secret: '%kernel.secret%'
    lifetime: 2592000
    guard:
    authenticators:
    - App\Security\LoginFormAuthenticator
    - App\Security\ApiTokenAuthenticator
    entry_point: App\Security\LoginFormAuthenticator

    # activate different ways to authenticate
    # https://symfony.com/doc/cur...

    # https://symfony.com/doc/cur...
    switch_user: true

    # Easy way to control access for large sections of your site
    # Note: Only the *first* access control that matches will be used
    access_control:
    - { path: ^/, roles: ROLE_USER }

    role_hierarchy:
    ROLE_ADMIN: [ROLE_ADMIN_COMMENT, ROLE_ADMIN_ARTICLE,ROLE_ALLOWED_TO_SWITCH]

  • 2019-09-12 adam

    security:
    # https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers
    providers:
    db_user_provider:
    entity:
    {
    class: App\Entity\User,
    property: email
    }
    encoders:
    App\Entity\User:
    algorithm: auto
    firewalls:
    dev:
    pattern: ^/(_(profiler|wdt)|css|images|js)/
    security: false
    main:
    anonymous: ~
    pattern: ^/
    http_basic: ~
    provider: db_user_provider
    form_login:
    login_path: app_login
    check_path: app_login
    default_target_path: /
    logout:
    path: app_logout
    target: /
    remember_me:
    secret: '%kernel.secret%'
    lifetime: 2592000
    guard:
    authenticators:
    - App\Security\LoginFormAuthenticator
    - App\Security\ApiTokenAuthenticator
    entry_point: App\Security\LoginFormAuthenticator


    # activate different ways to authenticate
    # https://symfony.com/doc/current/security.html#firewalls-authentication


    # https://symfony.com/doc/current/security/impersonating_user.html
    switch_user: true


    # Easy way to control access for large sections of your site
    # Note: Only the *first* access control that matches will be used
    access_control:
    - { path: ^/, roles: ROLE_USER }


    role_hierarchy:
    ROLE_ADMIN: [ROLE_ADMIN_COMMENT, ROLE_ADMIN_ARTICLE,ROLE_ALLOWED_TO_SWITCH]

  • 2019-09-12 Vladimir Sadicov

    Hey @adam

    Could you please share your config/packages/security.yaml contents?

    Cheers

  • 2019-09-11 adam

    Hi, after finishing this course all worked well except when am executing php bin/console debug:config NexySlackBundle or any other bundle name it returns this error:

    The guard authentication provider cannot use the "App\Security\LoginFormAuthenticator" entry_point because another entry point is already configured by another provider! Either remove the other prov
    ider or move the entry_point configuration as a root key under your firewall (i.e. at the same level as "guard").

  • 2019-07-16 Victor Bocharsky

    Hey Mike!

    Nope, actually, there's not any regularity, you can use whatever names you want. Well, you can make up your own personal convention and follow it in your project, but Symfony does not have anything related to it in best practices guide :)

    However, you can follow some simple conventions that may help you to figure out the correct URL, for example, prefix all your *admin* routes with "admin_" route name, or your api routes with "api_", etc. But that's jut up to you, just to help you easily navigate your route names when they will be a lot in your system :)

    Cheers!

  • 2019-07-14 Mike

    Is there any regularity/schema of the route naming?
    Sometimes you name is app_NAME and sometimes like here admin_NAME instead of app_admin_NAME

    Ian asking because you maybe have a tip for the naming schema.
    Thanks for your outstanding work Ryan!

  • 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.