Buy

Denying access is great... but we still have a User class that gives every user the same, hardcoded role: ROLE_USER:

95 lines src/AppBundle/Entity/User.php
... lines 1 - 12
class User implements UserInterface
{
... lines 15 - 46
public function getRoles()
{
return ['ROLE_USER'];
}
... lines 51 - 93
}

And maybe that's enough for you. But, if you do need the ability to assign different permissions to different users, then we've gotta go a little further.

Let's say that in our system, we're going to give different users different roles. How do we do that? Simple! Just create a private $roles property that's an array. Give it the @ORM\Column annotation and set its type to json_array:

112 lines src/AppBundle/Entity/User.php
... lines 1 - 12
class User implements UserInterface
{
... lines 15 - 40
/**
* @ORM\Column(type="json_array")
*/
private $roles = [];
... lines 45 - 110
}

Tip

json_array type is deprecated since Doctrine 2.6, you should use json instead.

This is really cool because the $roles property will hold an array of roles, but when we save, Doctrine will automatically json_encode that array and store it in a single field. When we query, it'll json_decode that back to the array. What this means is that we can store an array inside a single column, without ever worrying about the JSON encode stuff.

Returning the Dynamic Roles

In getRoles(), we can get dynamic. First, set $roles = $this->roles:

112 lines src/AppBundle/Entity/User.php
... lines 1 - 12
class User implements UserInterface
{
... lines 15 - 51
public function getRoles()
{
$roles = $this->roles;
... lines 55 - 61
}
... lines 63 - 110
}

Second, there's just one rule that we need to follow about roles: every user must have at least one role. Otherwise, weird stuff happens.

That's no problem - just make sure that everyone at least has ROLE_USER by saying: if (!in_array('ROLE_USER', $roles)), then add that to $roles. Finally, return $roles:

112 lines src/AppBundle/Entity/User.php
... lines 1 - 12
class User implements UserInterface
{
... lines 15 - 51
public function getRoles()
{
$roles = $this->roles;
// give everyone ROLE_USER!
if (!in_array('ROLE_USER', $roles)) {
$roles[] = 'ROLE_USER';
}
return $roles;
}
... lines 63 - 110
}

Oh, and don't forget to add a setRoles() method!

112 lines src/AppBundle/Entity/User.php
... lines 1 - 12
class User implements UserInterface
{
... lines 15 - 63
public function setRoles(array $roles)
{
$this->roles = $roles;
}
... lines 68 - 110
}

Migration & Fixtures

Generate the migration for the new field:

./bin/console doctrine:migrations:diff

We should double-check that migration, but let's just run it:

./bin/console doctrine:migrations:migrate

Finally, give some roles to our fixture users! For now, we'll give everyone the same role: ROLE_ADMIN:

28 lines src/AppBundle/DataFixtures/ORM/fixtures.yml
... lines 1 - 22
AppBundle\Entity\User:
user_{1..10}:
... lines 25 - 26
roles: ['ROLE_ADMIN']

Reload the fixtures!

./bin/console doctrine:fixtures:load

Ok, let's go see if we have access! Ah, we got logged out! Don't panic: that's because our user - identified by its id - was just deleted from the database. Just log back in.

So nice - it sends us back to the original URL, we have two roles and we have access. Oh, and in a few minutes - we'll talk about another tool to really make your system flexible: role hierarchy.

So, how do I Set the Roles?

But now, you might be asking me?

How would I actually change the roles of a user?

I'm not sure though... because I can't actually hear you. But if you are asking me this, here's what I would say:

$roles is just a field on your User, and so you'll edit it like any other field: via a form. This will probably live in some "user admin area", and you'll use the ChoiceType field to allow the admin to choose the roles for every user:

class EditUserFormType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder)
    {
        $builder
            ->add('roles', ChoiceType::class, [
                'multiple' => true,
                'expanded' => true, // render check-boxes
                'choices' => [
                    'Admin' => 'ROLE_ADMIN',
                    'Manager' => 'ROLE_MANAGER',
                    // ...
                ],
            ])
            // other fields...
        ;
    }
}

If you have trouble, let me know.

What about Groups?

Oh, and I think I just heard one of you ask me:

What about groups? Can you create something where Users belong to Groups, and those groups have roles?

Totally! And FOSUserBundle has code for this - so check it out. But really, it's nothing crazy: Symfony just calls getRoles(), and you can create that array however you want: like by looping over a relation:

class User extends UserInterface
{
    public function getRoles()
    {
        $roles = [];
        
        // loop over some ManyToMany relation to a Group entity
        foreach ($this->groups as $group) {
            $roles = array_merge($roles, $group->getRoles());
        }
        
        return $roles;
    }
}

Or just giving people roles at random.

Leave a comment!

  • 2017-05-24 Dennis

    Hi Victor Bocharsky,

    I did not have that indeed.
    I passed a string to the setRoles method. Now I've changed that, and adjusted the setRoles function to setRoles(array $roles)!

    Thanks!

  • 2017-05-22 Victor Bocharsky

    Hey Dennis,

    I suppose it's because in your code you're passing role as a string to setRoles() method, i.e. $user->setRoles('ROLE_ADMIN'). Check your setRoles() setter, in our project it has "setRoles(array $roles)" signature, which means your should pass an *array* of roles like: $user->setRoles(['ROLE_ADMIN']). If you still have some problems with it - please, show your setRoles() method declaration.

    Cheers!

  • 2017-05-21 Dennis

    Hello KNP,

    I've got a weird thing with the $roles json_array. I save the user to the database with this code:

    $user = new User();
    $user->setEmail('RyanIsAwesome@gmail.com');
    $user->setPlainPassword('SuperCoolPassword');
    $user->setRoles('ROLE_ADMIN');

    $em = $this->getDoctrine()->getManager();
    $em->persist($user);
    $em->flush();

    All goes well, and the user is saved into the database.

    Now when I login, It's not returning the $roles as an array, so it breaks on this line
    if(!in_array('ROLE_USER', $roles)) {

    Now I fixed it with this:

    $roles = (array)$this->roles;

    But why isn't it an array?

    Greetz, Dennis

  • 2017-04-06 Diego Aguiar

    Ohh that's very interesting!

    I found a quick guide of how you can force your user token to be realod, I haven't tested it, but it may work for you:
    http://www.pix-art.be/post/...

    Also check for "EquatableInterface", that's the key ;)

    Cheers!

  • 2017-04-06 Abou

    I have the same issue, I'll try your second solution i'l let you know :) !

  • 2017-04-06 Abou

    Thank's Diego.

    But I don't use a database to store those roles. I mean it's a CAS authentication. This is how it works :

    1 - My cas bundle handles the authentication. Set's a casUser (which has no addRole method).
    2 - I added a method : addRoles which I call in my listener :

    - { name: kernel.event_listener, event: kernel.controller, method: onKernelController }

    I mean I cannot log out and log in to refresh the security token. Because the app logic roles are never stored in a database, but guessed logically.
    So my problem now is how to refresh the securityToken because the "isGranted" method doesn't seem to know about the new added roles...

    Anyway i'm digging.
    Thank you all for the help.

  • 2017-04-04 Diego Aguiar

    Hey Abou!

    You are right about not manipulating bundle files directly, it's a bad practice, if you really have to do it, it's better to fork the repository, make your changes and make a pull request

    About adding roles dynamically, you can add a method to the User class like this:


    public function addRole($role){
    $role = strtoupper($role);

    if (!in_array($role, $this->roles, true)) {
    $this->roles[] = $role;
    }
    }


    And then create an admin area where you can manage your user's. Just remember, after adding a role to an user, if that user is logged in, he will need to logout/login to refresh his roles list.

    Oh, and thank you so much for your support, we really appreciate your interest in our tutorials!

    Have a nice day :)

  • 2017-04-04 Abou

    Hi everyone !

    I'm wondering if there's a way to add dynamic roles to a user outside the user class.

    I have a CAS authentication. So I use a bundle (the prayno one). For now I updated a file inside the bundle to add dynamic roles. But I don't like manipulating bundles files.

    So what I'm asking is : Is there a way to update the roles outside the authentication system and outside the CAS bundle ? I mean just after the Auth layer ? ... I guess I should use The "kernel.request" event, but how ?...

    Thank's !

    P. S. : Hi Ryan do you remember me ?... I promised to purchase a year subscription and guess what ? It's done :D...

  • 2017-03-16 Victor Bocharsky

    Hey Julien,

    Thank you for the kind words! Btw, we have a separate track for OOP if you're interested in it:
    https://knpuniversity.com/t...

    Cheers!

  • 2017-03-16 julien moulis

    Thank you so much. I realized that the pb was not (only) a lack of understanding of symphony, but OOP in general... I have to work more and more. By the way your courses are so great... I spent hours and hours on reading, searching and following different courses... But yours are so well executed and make me feel less dumb :-)!!!

  • 2017-03-15 Victor Bocharsky

    Hey Julien,

    Let me a bit improve and probably simplify my original idea: if you want to be able to set only one single role for your users, then you don't need User::$roles property at all, you just need the User::$role field. So your User entity may looks like:


    User
    {
    // other properties...

    /**
    * @ORM\Column(type="string")
    */
    private $role;

    public function getRole()
    {
    return $this->role;
    }

    public function setRole($role)
    {
    $this->role = $role;
    }

    // You have to keep this method due to the UserInterface
    public function getRoles()
    {
    return [$this->role];
    }

    public function setRoles(array $roles)
    {
    throw new \Exception('You probably never need to call it, but UserInterface enforce you to have this method')
    }

    // other methods...
    }

    How does it looks like? I.e. you replace User::$roles property with User::$role one. And then in your form just add a normal field for this $role property, so you event don't need "mapped => false" now.

    P.S. Sorry for misleading you in my first reply.

    Cheers!

  • 2017-03-15 julien moulis

    Hi Victor, tanks for the reply. Until I get your answer I used jquery. But I would like to use the virtual choice type method. But I can it make it... :-(
    The roles column in the database is empty.
    Where do I have to create the method? In the User entity or in the form?

  • 2017-03-15 Victor Bocharsky

    Hey Julien,

    There's a lot of ways to do it. If you're good at JavaScript, you could write a simple JS script which will allow you to select only one checkbox, i.e. reset all checkboxes except the latest. But probably there's another better solution: since UserInterface enforce you to have setRoles()/getRoles(), you can add a new "virtual" ChoiceType form field which name is "role", make it "mapped => false" and add User::setRole() method which will get a role string value, add it to an empty array and set this array to the $this->roles property overwriting the current role. The 3rd is more complex I think, but should work well too - choiceType with multiple false and create a Data Transformer for this field, which will convert single role string to an array and back: array with a single role to the single role string.

    Cheers!

  • 2017-03-14 julien moulis

    Hi, I would like to make the user select only one role. But with the choiceType with multiple false, it gives a string and then a nice error appears, as expected. Do you have a hint? Thx...
    By the way as you can guess I'm a newbie

  • 2017-01-27 mounir elhilali

    Thanks guys, is clear now :)

  • 2017-01-27 Victor Bocharsky

    Hey Mounir,

    Great! This's exactly the correct behavior. So ROLE_USER in this case is dynamicly added to all your users at runtime, even if they don't have any roles. It allows you do not worry about assigning default ROLE_USER to all your users. But if you add a different role to a user - it will be stored into the DB! So check if it works correctly by adding a new ROLE_ADMIN for the admin user, then persist() and flush() - you should see the only ROLE_ADMIN in database as we expected.

    Cheers!

  • 2017-01-27 mounir elhilali

    Sorry, i have just seen your reply.

    Yes i do use MySQL dababase.

    My getRoles() look like this:

    public function getRoles()
    {
    $roles = $this->roles;

    // give everyone ROLE_USER!
    if ( !in_array('ROLE_USER', $roles) ) {
    $roles[] = 'ROLE_USER';
    }

    return $roles;
    }

    I have no groups property in User class.

    when i dump $roles in getRoles() i get ['ROLE_USER']
    and when i dump $user->getRoles() in registerAction(), before persist method, i get ['ROLE_USER'].

    But dumping $user in registerAction(), before persist method returns roles with empty array.

    Thanks Victor!

  • 2017-01-26 Victor Bocharsky

    Hey Mounir,

    Check your setter/getter methods - maybe you just made a logic typo there. Also, try to debug roles before doing persist() and flush() with:


    dump($user->getRoles());
    die;
    $em->persist($user);
    $em->flush();

    Or if you set roles only in data fixtures, try to dump the result array which your getter returns:


    public function getRoles()
    {
    $roles = [];

    // loop over some ManyToMany relation to a Group entity
    foreach ($this->groups as $group) {
    $roles = array_merge($roles, $group->getRoles());
    }

    dump($roles); die;
    return $roles;
    }

    Btw, do you use MySQL database?

    Cheers!

  • 2017-01-26 mounir elhilali

    Hi Ryan/Victor,

    I get an empty array in roles database column! help plz

  • 2017-01-12 weaverryan

    I was surprised as well! And actually, I looked and there is an old issue about this: https://github.com/symfony/.... Our solution was also one posted there - so we were both thinking the right approach.

    Cheers!

  • 2017-01-10 samdjstevens

    Hey Ryan,

    Thanks for the reply - I've only just seen it now but I did implement pretty much exactly what you suggested in your second option!

    I did find it a bit weird that this behaviour wasn't supported at all and to achieve it you have to go about it in a round about way - surely role management of users with instant effect is a wide spread need?! Other than that Guard has been excellent to work with.

    Cheers,

  • 2017-01-10 weaverryan

    Hey samdjstevens!

    Yes, you understand things very well! For simplicity, I tell people that User::getRoles() is checked, but you're correct: this method is actually only called once on authentication. Those roles are then stored on the token, which is serialized from the session. The token is then deserialized from the session on each request, and those initial roles are used.

    So, how can you change this? Honestly, it looks a bit tricky: it's just *not* the normal behavior of the token. But, I can see 2 paths:

    1) Create your *own* token class (or extend whichever you're using now). Inside, override the setUser() method, and re-set the roles whenever this is called. setUser() is called at the beginning of the request and is passed the User object. So, it's a good spot to re-set the roles. But, you won't be able to extend AbstractToken anymore, because $roles is private - you'll need your own token. Also, depending on how you authenticate, overriding the token could be tricky - most of the time this is deep inside the authentication code. I think this would only be feasible if you're using Guard authentication, because then you can override the createAuthenticatedToken method (https://github.com/symfony/... to return a new token class.

    2) Another option - arguably better - is to create a listener on kernel.request. Give it a priority of 7, so that it runs *right* after the Firewall listener (which refreshes your User from the database). Inside, fetch the currently-authenticated User, and use it to instantiate a brand *new* token. That new token will of course have the new roles. Then, set it manually on the security.token_storage service class with $tokenStorage->setToken($newToken). This should work, as long as you make the new token the same class as the original and give it all the same data (e.g. if you originally have a PostAuthenticatedGuardToken, then re-use this and pass it the same args https://github.com/symfony/....

    Phew! Let me know if that helps! I'm a bit surprised it's that tough - your use-case makes good sense to me. It's possible I'm missing some built-in functionality, but I don't see it.

    Cheers!

  • 2017-01-10 samdjstevens

    Hi Ryan/Victor,

    I'm struggling with having the authenticated user's roles refreshed from the database on each page request. If I understand correctly, the roles are stored in the token, and that is what is checked against, (and not the return of the `getRoles()` method) on each page load.

    I've tried setting the `always_authenticate_before_granting` config option to true but this did nothing, tried implementing the `EquatableInterface`, which again did nothing.

    Any ideas on how I can have the user roles be refreshed from the database before any security checks are made?

    Cheers,

  • 2016-10-31 Daan Hage

    Aaah to bad. Thanks for the reply. I'll make a bash script then :)

  • 2016-10-31 weaverryan

    Hey Daan!

    This "shouldn't" happen, because the command calculates the correct order that the tables should be truncated. But, it *can* happen if you have some circular relationships (e.g. two tables - or maybe a bigger loop - that each have foreign keys that point back to each other). One way to fix that - if it's appropriate for your app - is to add some @JoinColumn(onDelete="CASCADE") (or SET NULL) functionality, so that when items in one of those relationships is deleted, it cascades in the database.

    But, if that's not really appropriate for your app (i.e. CASCADE would be potentially dangerous for that relationship in production), then you *will* need a workaround, as you suggested. There are no hook points that I know of, so I would recommend just making a simple shell script that runs all the commands for you (database:drop, database:create, migrations:migrate, fixtures:load).

    Cheers!

  • 2016-10-31 Daan Hage

    He Ryan/Viktor,

    A question regarding fixtures. ./bin/console doctrine:fixtures:load gives a fatal once i already have content, because of the foreign key constraints. This happens when it wants to truncate each table.

    Is there a way to either:
    1) set the order in which tables are truncated?
    2) or to execute a query before it runs the purge()-command?

    I know i can delete and recreate the entire database, but i find that a pretty lame workaround.

    Kind Regards!

  • 2016-10-28 Victor Bocharsky

    Hey Johan,

    Nice notice, thanks! Actually, we require only the latest 2.5 version of doctrine ORM due to the next line in composer.json: "doctrine/orm": "^2.5". So Composer won't use 2.6 for this project even when it'll be available.

    But I think we could add a note about it, I'll add it.

    Cheers!

    UPD: Note added in https://github.com/knpunive...

  • 2016-10-26 Johan

    Just a small note: The "json_array" type is deprecated since Doctrine 2.6. You should use "json" instead.

    Source: http://docs.doctrine-projec...

    EDIT: I actually just tried to use "json" and it does not even work yet, it says the type is not recognized. I don't know :l

  • 2016-09-15 Victor Bocharsky

    Hey Yang,

    Let me help you. Yes, it's definitely possible! The UserInterface just enforce you to have authentication-related fields on an entity, that's it. So you can add whatever you need fields to the User entity besides UserInterface required fields, even some relation properties like ManyToMany, etc.

    Yes, you're right! You wouldn't need to add these extra properties, but you could if it'll be useful for you, i.e. to build some complex join queries or just for convenience to get a collection of related entities.

    Cheers!

  • 2016-09-14 Yang Liu

    Hi Ryan,

    In my old blog system implemented with plain php, I had 3 tables for posts, comments and users. In post/comment-table theres a authorId column which points to an user. and in the user-table, infos like password and roles are stored. Now I want to rebuild this with symfony. So I would have post/comment entities, in post there will be a private $comment with OneToMany relation to the comment-entity and so on...
    my question ist now about user, is it possible to make an "class User implement UserInterface", so I have access to all the authentication stuff, and at the same time, have ManyToOne-relation in post/comment entities point to the user? something like

    /**
    * @ORM\ManyToOne(targetEntity="AppBundle\Entity\User")
    * @ORM\JoinColumn(nullable=false, onDelete="CASCADE")
    */
    private $user;

    Btw. I recall in the doctrine relationship tutorial, you said something about theres only ManyToOne or ManyToMany relationship. So I wouln't need to add extra properties like $comments or $posts in the user class?

  • 2016-08-19 Victor Bocharsky

    Hey James,

    In if statement you could check whether the current user has a role ROLE_ADMIN or the current user is owner (like in Ryan's example) - then allow access:


    public function showAction($id)
    {
    $user = // query for the User

    $this->denyAccessUnlessGranted('ROLE_ADMIN');

    if ($user != $this->getUser()) {
    throw $this->createAccessDeniedException('this is not your account!');
    }
    }

    Cheers!

  • 2016-08-19 james

    Voters is it ! Thanks again! But if I still want admin to have access, how do I add the admin to the if statement? And is there some king of super admin for developers like you talked about in one of you tuto?

    Thanks

  • 2016-08-18 weaverryan

    Hi James!

    I'm not 100% sure I'm going to answer what you're actually asking, but let me give it a shot - then you can tell me that I didn't fully understand your question if I miss :).

    Giving each user a different role is cool, but it's still a "global" property - it helps to answer questions like "should the user have access to the blog admin area in general". But, roles can't be used for security that's based on an *object* - e.g. perhaps a user should have access to only the blog posts that *they* author. So, they will have access to edit *some* blog posts, but not others. Is this kind of what you're asking?

    In short, there are 2 ways to solve this:

    1) Manually (I don't often do this, but you'll see how simple it is). For example, suppose there is some url like /users/{id}, but I should only be able to see *my* page. Then, you might do this in the controller:


    public function showAction($id)
    {
    $user = // query for the User

    if ($user != $this->getUser)) {
    throw $this->createAccessDeniedException('this is not your account!');
    }
    }

    So, you can simply put whatever security logic you need in your controller and then deny access like this.

    2) A better solution is to use voters: https://knpuniversity.com/s.... These are very similar to doing what I just did above, but you centralize your security logic so that you can more easily re-use it.

    Does this help? Or did I totally answer the wrong question? :)

    Cheers!

  • 2016-08-18 james

    Just a quick one, if I add different users which have different accounts with their details and their favorites etc.. How would you protect each new users account. I would say dynamically but I am not sure how, maybe with their ID I suppose?

  • 2016-08-12 Richard Perez

    Thanks guys, is clear now :)

    Regards,

  • 2016-08-11 weaverryan

    Hey Richard!

    Ah, that makes more sense :). Or, more specifically - you mean @Security("ROLE_ADMIN").

    It's more or less perfectly equivalent. The ^/admin in the firewall causes some code to run that compares the URL and if matches, checks for ROLE_ADMIN. If you use @Security, it does the same thing - except instead of checking if the URL matches, it checks to see if that controller class is being executed on this request.

    So it comes down to a matter or preference: do you like protecting by URL better? Or do you like to secure specific controller classes. It's nice to have both options :).

    Cheers!

  • 2016-08-11 Richard Perez

    Hi Victor, sorry my bad, What I want to say, was, what is the different about using @Security(/admin) vs using in the firewall ^/admin.

  • 2016-08-10 3amprogrammer

    Hmm. I was used to roles and permissions structure. I mean, used to have a roles table, permissions table, pivot tables between them, user could have roles but also a independent permissions and so on. I was using https://github.com/romanbic... with Laravel.

    How this relates to Symfony? Do groups act as roles and roles become permissions in Symfony world? If a role represents a set of permissions, hard coded in my application (according to: http://stackoverflow.com/qu... ) how am I supposed to associate permissions with roles?

  • 2016-08-10 Victor Bocharsky

    Hey Richard,

    That's different things. The "@Route" annotation at the top of any controller class is just a prefix for all routes, which this controller has. It means, that if you have:


    /**
    * @Route("/admin")
    */
    class GenusController
    {
    /**
    * @Route("/genus")
    */
    public function indexAction()
    {
    // code here...
    }
    }

    so the `indexAction()` URL will be the "/admin/genus" (i.e. with "/admin" prefix) but not just the "/genus". That's it!

    Firewall in turn allows you to configure who has access to a page.

    Cheers!

  • 2016-08-09 Richard Perez

    Ryan, what would be the different doing the @Route(/admin) at the top of the controller vs using the firewall? And if in this case there are no different, what is the advantage of using on or other.

    Thansk

  • 2016-08-02 weaverryan

    Hi Andjii!

    This error means one very specific thing: Symfony cannot find a route that matches the current URL. So, it definitely has nothing to do with your form or your controller: the problem is *definitely* your route.

    Can you tell post your Route annotation code and also tell me what URL you're going to? Also, try running the "bin/console debug:router" command to see if your route shows up there.

    Cheers!

  • 2016-08-01 Andjii

    I created class EditUserFormType which extends AbstractType, as in example. It is placed at "Form" folder. I added @Route with proper route, but no form got, only "No rout found". Where I've done mistake?