Wednesday, September 12, 2012

Symfony 2 and the case of the missing ROLE_PREVIOUS_ADMIN role

In the Symfony 2 manual there's a small part about 'Impersonating a User', which talks about how you can switch between users if you are an administrator. After following the steps, as discussed in the manual, I wanted to find a way to see if the current logged in user was an impersonation, or the real user. After searching on Google for a bit, I found a couple of posts (1, 2, 3) that all described that the impersonated user should have the role 'ROLE_PREVIOUS_ADMIN'. But for some case my impersonated users did not have that role.
So the search for the missing ROLE_PREVIOUS_ADMIN role started.While looking to the logs in the Symfony Profiler, I noticed a weird line that kept popping up:
Username "" was reloaded from user provider.
While digging trough the Symfony 2 code I noticed something quite remarkable: When you write your own UserProviderInterface as described in the Symfony 2 Cookbook the function 'loadUserByUsername' is never called, even though you have to overload it. Instead the EntityUserProvider's function is called instead. Symfony loops over the different UserProviders, but uses  the first one to load the user and then exits.
This discovery came together with the solution for the previous mentioned problem, but this requires a bit of history.

Earlier during development I ran into the problem that my user object coulnd't be serialized (I don't recall the exact problem). When I searched the error on Google the mentioned solution was to write your own serialize and unserialize functions using just a users ID:
    public function serialize()
    {
        return serialize($this->id);
    }

    public function unserialize($data)
    {
        $this->id = unserialize($data);
    }
The serialized data of the user object is what is getting stored in your session, so when it is restoring the user from the token, it only has its id and not its username (causing loadUserByUsername to fail). Therefore the (un)serialize code should be something like the following:
    public function serialize()
    {
        return serialize(array($this->id,$this->username));
    }

    public function unserialize($data)
    {
        list($this->id,$this->username) = unserialize($data);
    }
In fact it would only need the username to be serialized!

With the username being present in the unserialized user, the loadUserByUsername function succeeds and also keeps the roles set in the serialized user.

3 comments:

  1. It worked, thanks!

    BTW, you require serializing the id if you use EntityUserProvider.

    ReplyDelete
    Replies
    1. I knew there was a place where I got the example that only serialized the ID, now I remember where! Added a pullrequest for the docs https://github.com/symfony/symfony-docs/pull/2223

      Delete
  2. Hi Tom, I have the same problem. But I use the email to connect users.
    I tried to replace username by email in serialize method but it doesn't work.

    Have you any idea?

    ReplyDelete