user4112557
user4112557

Reputation:

Symfony 4 - Define custom user roles from DB

I am learning Symfony 4, and I am lost with user roles and I don't know where to start. Because it will be an intranet like website Users will not register themselves, the admin will register them and set the role. The admin can also create new roles dynamically. I would like to store user roles in database. I have 2 entities User and Role. (Relations are not defined yet, that´s normal)

Here is my User entity :

<?php

namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Security\Core\User\UserInterface;

/**
 * @ORM\Entity(repositoryClass="App\Repository\UserRepository")
 * @UniqueEntity(fields="email", message="Email already taken")
 * @UniqueEntity(fields="username", message="Username already taken")
 */
class User implements UserInterface, \Serializable
{
    /**
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     * @ORM\Column(type="integer", unique=true)
     */
    private $id;

    /**
    * @ORM\Column(type="string", length=255)
    */
    private $firstname;

    /**
     * @ORM\Column(type="string", length=255)
     */
    private $lastname;

    /**
     * @ORM\Column(type="string", length=255)
     */
    private $email;

    /**
     * @ORM\Column(type="date")
     */
    private $birthdate;

    /**
     * @ORM\Column(type="integer", nullable=true)
     */
    private $roleId;

    /**
     * @ORM\Column(type="string", length=255, nullable=false)
     */
    private $username;

    /**
     * @Assert\NotBlank()
     * @Assert\Length(max=4096)
     */
    private $plainPassword;

    /**
     * @ORM\Column(type="string", length=255, nullable=false)
     */
    private $password;

    /**
     * @ORM\Column(name="is_active", type="boolean", nullable=true)
     */
    private $isActive;

    // GETTERS

    public function getId()
    {
        return $this->id;
    }

    public function getFirstname()
    {
        return $this->firstname;
    }

    public function getLastname()
    {
        return $this->lastname;
    }

    public function getBirthdate()
    {
        return $this->birthdate;
    }

    public function getEmail()
    {
        return $this->email;
    }

    public function getRoleId()
    {
        return $this->roleId;
    }

    public function getRoles()
    {
        return array('ROLE_USER');
    }

    public function getUsername()
    {
      return $this->username;
    }

    public function getPlainPassword()
    {
        return $this->plainPassword;
    }

    public function getPassword()
    {
      return $this->password;
    }

    public function getSalt()
    {
     return null;
    }

    // SETTERS

    public function setFirstname($firstname)
    {
        $this->firstname = $firstname;
    }

    public function setLastname($lastname)
    {
        $this->lastname = $lastname;
    }

    public function setEmail($email)
    {
        $this->email = $email;
    }

    public function setBirthdate($birthdate)
    {
        $this->birthdate = $birthdate;
    }

    public function setRoleId($roleId)
    {
        $this->roleId = $roleId;
    }

    public function setUsername($username)
    {
        $this->username = $username;
    }

    public function setPlainPassword($password)
    {
        $this->plainPassword = $password;
    }


    public function setPassword($password)
    {
        $this->password = $password;
    }

    // FUNCTIONS

    public function serialize()
    {
        return serialize(array(
            $this->id,
            $this->username,
            $this->password,
            // see section on salt below
            // $this->salt,
        ));
    }

    /** @see \Serializable::unserialize() */
    public function unserialize($serialized)
    {
        list (
            $this->id,
            $this->username,
            $this->password,
            // see section on salt below
            // $this->salt
        ) = unserialize($serialized);
    }

    public function eraseCredentials()
    {

    }
}

Here is my Role entity :

<?php

namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity(repositoryClass="App\Repository\RoleRepository")
 */
class Role
{
    /**
     * @ORM\Id
     * @ORM\GeneratedValue
     * @ORM\Column(type="integer", unique=true)
     */
    private $id;

    /**
     * @ORM\Column(type="string", length=255, nullable=false)
     */
    private $type;

    // GETTERS

    public function getId()
    {
        return $this->id;
    }

    public function getType()
    {
        return $this->type;
    }

    // SETTERS

    public function setType($type): void
    {
        $this->type = $type;
    }
}

Is it a good practice to do so ??

On the Symfony documentation, we can read everywhere the "macros" ROLE_USER, ROLE_ADMIN... Where are they defined ? Can we customize this ?

Upvotes: 5

Views: 20182

Answers (2)

Juanmi
Juanmi

Reputation: 71

Recommended Reading from current Symfony Documentation (version 4 at the time of this answer): https://symfony.com/doc/current/security.html#hierarchical-roles

Symfony recommends defining role inheritance, please see if it helps; from the link just above,

" Instead of giving many roles to each user, you can define role inheritance rules by creating a role hierarchy:

# config/packages/security.yaml
security:
# ...

role_hierarchy:
    ROLE_ADMIN:       ROLE_USER
    ROLE_SUPER_ADMIN: [ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH]

Users with the ROLE_ADMIN role will also have the ROLE_USER role. And users with ROLE_SUPER_ADMIN, will automatically have ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH and ROLE_USER (inherited from ROLE_ADMIN).

For role hierarchy to work, do not try to call $user->getRoles() manually. For example, in a controller extending from the base controller:

// BAD - $user->getRoles() will not know about the role hierarchy
$hasAccess = in_array('ROLE_ADMIN', $user->getRoles());

// GOOD - use of the normal security methods
$hasAccess = $this->isGranted('ROLE_ADMIN');
$this->denyAccessUnlessGranted('ROLE_ADMIN');

" end of copying

I think storing the role as a string is enough if inheritance fits your needs.

Upvotes: 3

dbrumann
dbrumann

Reputation: 17166

Symfony used to support role entities with a RoleInterface, much like with your user implementing the UserInterface. It was decided that this is unnecessary as the roles-table will usually only contain 2 fields (id, name) which means we might just as well store the roles directly in the users table.

So, if you want to follow Symfony best practices you would just have your roles in the user, for example like this:

class User implements UserInterface
{
    /** @Column(type="json") */
    private $roles = [];

    public function getRoles(): array
    {
        return array_unique(array_merge(['ROLE_USER'], $this->roles));
    }

    public function setRoles(array $roles)
    {
        $this->roles = $roles;
    }

    public function resetRoles()
    {
        $this->roles = [];
    }
}

If you don't want to store the roles as JSON-encoded string you can also use @Column(type="array") */ or write a custom DBAL type, e.g some kind of EnumType. You might also want to secure the setter against invalid data or add validation.

Regarding the second question about the roles used in the documentation: Symfony has some predefined roles and pseudo-roles for certain features:

  • IS_AUTHENTICATED_ANONYMOUSLY
  • IS_AUTHENTICATED_REMEMBERED
  • IS_AUTHENTICATED_FULLY
  • ROLE_ALLOWED_TO_SWITCH (for user switching)

The other roles like ROLE_USER and ROLE_ADMIN are purely exemplary and you may use them as you see fit. You do not even have to start your own roles with a ROLE_ (although some security features rely on this convention and things will not work as expected for those roles unless you do some manual changes).

Upvotes: 19

Related Questions