Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

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!

68
Login or Register to join the conversation
Dung L. Avatar
Dung L. Avatar Dung L. | posted 3 years ago

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!

1 Reply
ties8 Avatar

Hello dear People at Symfony Casts, let's say i want to create a streaming website, everyone has a channel they own, and they can add moderators, administrators, vips and subscribers to their channel. Do i use the role system for such a project? Like for example ROLE_(channelid)_MODERATOR stored in the users roles, or is using a custom voter and storing the role inside the channel <-> user relation entity the way to go? Or is there an entirely different system i can use for this job? I look forward to your answer!
- yours kindly, Alex

Reply

Hey Alex T.

I think storing those roles on the User object is a good approach if you want em to be persisted. What I mean is the next time the stream goes live, you don't want to reassign all the moderators, then you should store the roles on the User object.

I'm not sure if Symfony is a good fit for a real-time application like yours but perhaps I'm wrong. Let me know if you disagree with me.
Cheers!

Reply
ties8 Avatar

I think i expressed myself wrong, i took the stream site as an example, i am looking for the best way to build a roles system which is not site wide, like the Admin roles in "The Space Bar" project but only relevant for a small part of the app. I guess Google Tag Manager is also a good example of such a system, there are hundred tousands of containers in googles system, and with my google account i have full rights to my Containers, on each of my containers i have an admin role and can add other users and assign them privileges/roles, like full access, editorial access or readonly access. I imagine in a system like that in symfony would have a global User Entity, a Container entity and a third entity with user_id and container_id, essentially a ManyToMany Relation between those two. Now i wanted to know how i best store permissions in a system like described above, i imagined i could generate roles for each container on the fly, for example "ROLE_CONTAINER". $containerID ."_ADMIN" so i would have ROLE_CONTAINER1_ADMIN, ROLE_CONTAINER1_MOD, ROLE_CONTAINER2_ADMIN ect... for as many containers as there are, and store all of them in the global user entity, but in your tutorials you always seem to use static strings as Role keys, so i wanted to ask if my idea, to generate roles for each container is bad practice? As an alternative i thought i could add a Field named Container_Roles to the relation between Container and User and store the roles there, for example in the relation between user1 and container1 user1 has the role admin, user2 has the role of moderator in his relation to container1 ect. But if i use a system like that i would have to probably code a Voter and could no longer use the features of symfonys role system. So i see many ways to make a system like that but have no idea what is bad or good practice with symfony or what the "establishment" uses for such a permission system in symfony. I hope you understand my question better now.

Reply

Hey Alex T.

I think I get your point better now. I think the solution depends on how you pretend to manage the roles, dynamically or statically. By statically I mean, you will define the roles hierachy in the config/packages/security.yaml file and just use the isGranted() function to determine if a User object has a role or not.
Dynamic roles are like in your example, where you need to check external resources, for example, another table in your database, or a third party service, in this situation you need to write a Voter and add all the "authorization" logic there

Reply
Gunjeet K. Avatar
Gunjeet K. Avatar Gunjeet K. | posted 2 years ago | edited

Hello,

Am using Symfony new Authenticator based Security introduced in Symfony5.1. Though it is in experimental phase but since it was simpler to implement as compared to old one I went with it. But am facing issue in implementing access controls. If I give PUBLIC_ACCESS to a given route in security.yaml. I was assuming it will not go through authentication in first go and will be given public access. However it always go through the process of authentication and I get 401.
Here is my security.yaml settings:


security:
    enable_authenticator_manager: true
    firewalls:
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false
        main:
            lazy: true
            custom_authenticators:
                - App\Security\BridgeLegacyAppAuthenticator
    access_control:
        - { path: ^/api/test, roles: PUBLIC_ACCESS }

So as mentioned /api/test does not get PUBLIC_ACCESS. Kindly let me know If am doing something wrong. Thank You!!

Reply

Hey Gunjeet K.

As mentioned here in the docs https://symfony.com/doc/cur... by using the PUBLIC_ACCESS role it should allow anon users to visit that page so I believe there's something else going on. Do you have a custom voter or something similar?
What happens if you make your whole site public?

Cheers!

Reply
Gunjeet K. Avatar

Hello Diego Aguiar,

Thanks a lot for the reply!!
I get 401 even if I give PUBLIC_ACCESS to whole site. I have not yet implemented voters.
After adding authentication I wanted to give few pages public access and am stuck on it.
Probably there is an issue with my authentication implementation. Inside authenticate() am looking up for user in DB and if not found I throw AuthenticationException().
Since authentication is first step and then authorization. If am not logged In and I try to access any page of the site, Authenticator throws the exception and it never reaches access control . Am wondering if this is the right flow.
If the user exist, I return passport instance else exception. I thought of using symfony's AnonymousPassport class to represent anonymous users but found it is of internal use.

Thank You.

Reply

Hey Gunjeet K.

Sorry for my late response, holidays got in the middle. I believe the problem relies on your support() method of your Authenticator, it should return true only when you want to authenticate the incoming request. For example, when you got a POST request to the login page

Cheers!

Reply
Gunjeet K. Avatar
Gunjeet K. Avatar Gunjeet K. | MolloKhan | posted 2 years ago | edited

Thanks Diego.
Yeah the problem is with my support(). Actually my requirement is to share session with legacy application. I tried to use:


$session = new Session(new PhpBridgeSessionStorage());
$session->start();

But this does not work. I found the below code in stackoverflow and it worked:


        $storage = new NativeSessionStorage();
        $session = new Session($storage, new NamespacedAttributeBag());

        $session->start();
        $session->replace($_SESSION);
        $session->save();
        $request->setSession($session);

        $session = new Session(new PhpBridgeSessionStorage());
        $session->start();

But then session does not persist.
Am struggling with sharing sessions and authentication. I don't find anything for PhpBridgeSessionStorage in SymfonyCasts. Kindly help.

Thanks!!

Reply
Gunjeet K. Avatar

Any help on this will be really appreciated. I still have not been able to make it work :(
Thank You.

Reply

Hey Gunjeet K.!

This sounds interesting! It's not something I've specifically had to do before, but yes, my impression is that this is precisely what PhpBridgeSessionStorage is for (which no, we don't have anything about in SymfonyCasts).

Have you tried the config in the docs about this? https://symfony.com/doc/cur... Basically, you shouldn't need to instantiate the session storage objects manually: Symfony already maintains these as services. What you need to do is instruct Symfony to use the PhpBridgeSessionStorage as its "storage" service. Specifically, I'm thinking the 2nd example on that page may be what you're looking for, but I'm not sure: it depends on your situation :).

Btw, you said:

> Actually my requirement is to share session with legacy application

That can mean a few thing. It could mean that you are booting a Symfony app AND legacy PHP app code at the same time... and so (perhaps) the legacy code is already starting the session. That's, I believe, what the PhpBridgeSessionStorage is for.

OR, you might be saying that some requests go to the legacy app and some go to the new Symfony app (but they are never both executing at the same time). In that case, it's a different situation. And also, what exactly are you trying to share? Does the user log in to the legacy site and you need to know that they are logged in on the new site? Or the opposite?

Cheers!

Reply
Gunjeet K. Avatar

Hello @weaverryan,

Thanks for the reply!!. My scenario is somewhat like the last one you mentioned where some requests go to legacy and some to new app and the login always happens through legacy app. Though as of now requests to new and legacy app does not happen at a same time but going forward might be legacy app is calling new app REST API and vice versa.

> what exactly are you trying to share? Does the user log in to the legacy site and you need to know that they are logged in on the new site?
Correct we want to keep the user authenticated in new site as well. And we want to share other session data as well. So if my new app sets some data in session. It should be accessible in legacy and vice-versa.

My both apps are running on same apache server where requests beginning with say /sf/ are redirected to new Symfony app rest goes to legacy. We are using memcache to save sessions.

As mentioned I have tried PhpBridgeSessionStorage , it does not work. Inside Symfony request, If I grab the session Id from cookie and look up in memcache, I see the session data there. Not sure how to make Symfony look up that sessionId and populate its session bags. I tried using MemcachedSessionHandler but it too does not work.

Thanks.

Reply

Hey Gunjeet K.!

Excellent! It makes sense now! i think this issue is discussing the problem: https://stackoverflow.com/questions/35601629/symfony-2-and-custom-session-variables-from-legacy-application

Basically, when you use the Session object in symfony, it is reading from a sub-key on the session. So even if you verified - by var_dump($_SERVER) in the new app - that the session was being shared, the keys would not show up in Symfony's session object.

I'm not exactly sure what the best solution is... most solutions seem to add a listener early on the request to read the session data from the legacy app via $_SESSION and put it into the Session object. That's... a bit odd (since you're duplicating the data), but as long as the new app is only ever reading that data (and never changing it), I don't see any real issue.

Let me know if this helps :).

Cheers!

Reply

I have completed this course. Security bundle is really awesome. Thanks symfony cast.

Reply

Hey Abusayeed!

You nailed it, congratulations! We're really happy to hear you liked this course :)

Cheers!

Reply

My code in controller


        if ($this->isGranted('MANAGE_GROUP', $group)) {
           ...................................
        }

In voter


    protected function supports($attribute, $subject)
    {
        return in_array($attribute, ['MANAGE_GROUP'])
            && $subject instanceof Group;
    }

    protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token)
    {
     
        switch ($attribute) {
            case 'MANAGE_GROUP':
            
                return false;
        }

        return false;
    }

But when I execute the code above my request enters the "support" function. I have tested with dd.
But does not reach 'voteOnAttribute' function. Rather it passed the authentication.
I have tested with 'dd', and found code did not enter this method.

why??

Reply

Hey Abusayeed,

Try to check with dd() what the supports() method return, does it return true? It should call voteOnAttribute() only if the support method return true. You might misprint the namespace of Group so it will always return false for you.

Cheers!

Reply
Hicham A. Avatar
Hicham A. Avatar Hicham A. | posted 3 years ago

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

Reply

Hey Hicham A.!

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!

Reply

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') %}<br />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 !

Reply

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!

Reply

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 !
Reply

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!

Reply

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!

Reply

Hey Victor,

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

Cheers!

1 Reply

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!

Reply
Robert V. Avatar
Robert V. Avatar Robert V. | posted 3 years ago

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!

Reply

Hey Robert V.

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!

Reply
Robert V. Avatar

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!

1 Reply

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?

Reply

Hey truuslee!

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!

Reply
Oleksandr K. Avatar
Oleksandr K. Avatar Oleksandr K. | posted 3 years ago

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 ?

Reply

Hey Oleksandr K.!

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!

2 Reply
Oleksandr K. Avatar
Oleksandr K. Avatar Oleksandr K. | weaverryan | posted 3 years ago | edited

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!

1 Reply

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!

2 Reply
Oleksandr K. Avatar

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

Reply
Mouad E. Avatar
Mouad E. Avatar Mouad E. | posted 3 years ago | edited

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 <br /> ider or move the entry_point configuration as a root key under your firewall (i.e. at the same level as "guard").

Reply

Hey @adam

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

Cheers

Reply
Mouad E. Avatar

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]

Reply
Mouad E. Avatar
Mouad E. Avatar Mouad E. | sadikoff | posted 3 years ago | edited

`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]`
Reply

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

Cheers

Reply
Mouad E. Avatar

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

Reply

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!

Reply
Mike P. Avatar
Mike P. Avatar Mike P. | posted 4 years ago

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!

Reply

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!

Reply
Default user avatar
Default user avatar elbarto | posted 4 years ago

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

Reply

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!

Reply
elbarto Avatar
elbarto Avatar elbarto | MolloKhan | posted 4 years ago | edited

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;

`

<u>So my question is how would you deal with that ?</u>

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 !

Reply

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!

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.1.3",
        "ext-iconv": "*",
        "composer/package-versions-deprecated": "^1.11", // 1.11.99
        "knplabs/knp-markdown-bundle": "^1.7", // 1.7.0
        "knplabs/knp-paginator-bundle": "^2.7", // v2.8.0
        "knplabs/knp-time-bundle": "^1.8", // 1.8.0
        "nexylan/slack-bundle": "^2.0,<2.2.0", // v2.0.0
        "php-http/guzzle6-adapter": "^1.1", // v1.1.1
        "sensio/framework-extra-bundle": "^5.1", // v5.2.0
        "stof/doctrine-extensions-bundle": "^1.3", // v1.3.0
        "symfony/asset": "^4.0", // v4.1.4
        "symfony/console": "^4.0", // v4.1.4
        "symfony/flex": "^1.0", // v1.17.6
        "symfony/framework-bundle": "^4.0", // v4.1.4
        "symfony/lts": "^4@dev", // dev-master
        "symfony/orm-pack": "^1.0", // v1.0.6
        "symfony/security-bundle": "^4.0", // v4.1.4
        "symfony/serializer-pack": "^1.0", // v1.0.1
        "symfony/twig-bundle": "^4.0", // v4.1.4
        "symfony/web-server-bundle": "^4.0", // v4.1.4
        "symfony/yaml": "^4.0", // v4.1.4
        "twig/extensions": "^1.5" // v1.5.2
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.0", // 3.0.2
        "easycorp/easy-log-handler": "^1.0.2", // v1.0.7
        "fzaninotto/faker": "^1.7", // v1.8.0
        "symfony/debug-bundle": "^3.3|^4.0", // v4.1.4
        "symfony/dotenv": "^4.0", // v4.1.4
        "symfony/maker-bundle": "^1.0", // v1.7.0
        "symfony/monolog-bundle": "^3.0", // v3.3.0
        "symfony/phpunit-bridge": "^3.3|^4.0", // v4.1.4
        "symfony/profiler-pack": "^1.0", // v1.0.3
        "symfony/var-dumper": "^3.3|^4.0" // v4.1.4
    }
}
userVoice