Search code examples
symfonyauthorizationsymfony4doctrine-odm

Symfony 4 Voters - Roles and Permissions Questions


I started working on a Voter System to secure my URLs and I have many questions.

First, I don't know if I have to work with "Roles" or with "Permissions".

By "Roles" I mean something like Role_User, Role_Manager, etc. And by "Permissions", I mean something like "Access_users", "Edit_User", "Add_User", etc.

Am I right with that ?

If yes, I'd like to check Permissions in my Voters to control the access to URL, event if I don't have an object to control.

Example

/**
 * @Route("/", name="list")
 * @Security("is_granted('ROLE_ADMIN') or is_granted('PERM_ACCESS_SERIES')")
 */
public function list()
{
    $series = $this->seriesService->findAll();

    return $this->render('backend/series/list.html.twig', [
        "series" => $series
    ]);
}

In this case, I want to check if the user have the role ROLE_ADMIN or the permission PERM_ACCESS_SERIES, but I don't know how to do it in my Voters.

For information, the 'ROLE_ADMIN' is set in my security.yaml config, but I'd like to make these roles dynamic, to be able to create roles (and assign permissions to role?).

In my Voter class, when I control on a Document, I check the owner, and other Users that can manage the Document, but when I don't pass a Document, I don't really know what to do.

Is just get the user's permissions and check if the permission asked is into them. something like that :

if (in_array($attribute, $user->getPermissions())) {
    return true;
}

Actually here is my voter

 protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
{
    //ADMIN and SUPER_ADMIN can do anything they want !
    if ($this->decisionManager->decide($token, array(User::ROLE_ADMIN, User::ROLE_SUPER_ADMIN))) {
        return true;
    }

    $user = $token->getUser();
    if (!$user instanceof User) {
        // the user must be logged in. if not, deny access
        return false;
    }

    $series = $subject;
    switch ($attribute) {
        case self::PERM_ACCESS_SERIES:
            if (in_array($attribute, $user->getPermissions())) {
                return $this->canAccess($series, $user);
            }
            break;

        case self::PERM_ADD_SERIES:
            if (in_array($attribute, $user->getPermissions())) {
                return $this->canAdd($series, $user);
            }
            break;

        case self::PERM_EDIT_SERIES:
            if (in_array($attribute, $user->getPermissions())) {
                return $this->canEdit($series, $user);
            }
            break;

        case self::PERM_DELETE_SERIES:
            if (in_array($attribute, $user->getPermissions())) {
                return $this->canDelete($series, $user);
            }
            break;

        default:
            throw new \LogicException('This code should not be reached!');
            break;
    }

    return false;
}

On each case, I check if the attribute is in the user's permissions.

Well, as you can see, I lack a lot of sense about Roles and Permissions, so advices and help to organize all that are welcomed.

Also, if something is unclear don't hesitate to tell me, English is not my native language so maybe My sentences are not well constructed and completely incomprehensible.


Solution

  • What you saying Roles and Permissions in your way are the same thing. No need to distinguish between them. You can think of your PERM, like ROLE_ACCESS_SERIES and so on, its just matter of naming / conventions but from Symfony point of view its the same.

    Assigning permissions to roles, isn't nothing else than role hierarchy, more in documentation. Dont forget as well, that one user can have multiple roles.

    From now I will talk only about roles.

    For dynamic roles, your UserInterface object has method getRoles() which doesn't have to be static, but can for example load roles from database / any storage you want for each user.

    For object to check in permission. For example ROLE_LIST_SERIES doesn't usually need any object to check permission on, so its ok without object, but ROLE_EDIT_SERIES for example should have always object which we are editing associated, means if no is provided we throw exception and it means we are missing somewhere place to pass it.

    For me, if we take for example CRUD I will have roles like this:

    ROLE_ENTITY_MASTER:
        - ROLE_ENTITY_LIST
        - ROLE_ENTITY_EDIT
        - ROLE_ENTITY_ADD
        # and you continue what you need.
    

    Advantage of this is, that for example if I wanna give somebody full access to something I just need to assign one role and if I want somebody restrict I give him only roles I want. Then in my getRoles() (in reality I'm loading from database, so this is just pseudocode).

    getRoles() {
        return [ROLE_ADMIN, ROLE_ENTITY_MASTER2, ROLE_ENTITY_MASTER];
    }