8
votes

I make use of SonataAdminBundle in Symfony 3. Because I use Symfony 3, I still can't make use of SonataUserBundle. So I am using SonataAdminBundle with FOSUserBundle only.

Now what I try to achieve is to hide specific routes per role. For example, I only have three roles;

  • Super Admin
  • Admin
  • Another role

Super Admin has all the roles admin has, admin has all of the third one, and the third one has ROLE_USER obviously. Super Admin should be able to create new users and assign a role to him. The Super Admin should also be able to change user's passwords. The users should be able to change the passwords of their own accounts. And finally, other roles that Super Admin should not be able to change their own roles and to create new users.

How can I achieve this without using SonataUserBundle. For the removing of routes part I tried something like this:

protected function configureRoutes(RouteCollection $collection)
{
    $securityContext = $this->getConfigurationPool()->getContainer()->get('security.authorization_checker');

    if (!$securityContext->isGranted('ROLE_SUPER_ADMIN')) {
        $collection->remove('create');
        $collection->remove('edit');
    }
}

But I guess there is a better solution. I am completely aware of the official documentation about security but I'm confused with that, does that mean I have to hard code each and every single role for all different Admins in my security.yml file? Does this even work without SonataUserBundle? I don't want to add extra database tables for ACL.

Can somebody please assist and/or provide a good example? I'll really appreciate it a lot.

2

2 Answers

3
votes

How to manage users and roles in Sonata without SonataUserBundle?

Answer: we need to do the same as SonataUserBundle. (But let's simplify a little)

An analogy about security based on ROLE_ in Symfony flat:

  • The house: A building that has doors and keys (the system).
  • The door: Place in the house where access is restricted - isGranted():

    // the door is here, we need the key to open it.
    if ($this->isGranted('ROLE_FOO')) {
        // restricted access to do something
    } 
    
  • The key: Granted permission to access a restricted door - ROLE_*:

    class User extends FOSUser
    { 
        public function getRoles()
        {
            // the keys comes from DB or manually.
            // e.g: 
            return ['ROLE_FOO'];
        }
    }
    
  • The master key: A key that can open several doors:

    # app/config/security.yml
    
    security:
        role_hierarchy:
            # other than opening the door "isGranted('ROLE_BAR')"
            # we can also opening the door "isGranted('ROLE_FOO')" with this single key.
            ROLE_BAR: ROLE_FOO
    

Following this analogy, SonataAdminBundle already has created the doors to restrict access to each default action (e.g. list action) across an entity managed.

So our job is to assign the keys to users "only" (unless you need to create your own doors). There are many ways to achieve this (it'll depend on what you need).

Note: If you don't have a role hierarchy, you have single keys only (i.e. you don't have master keys), which makes it less flexible assignment of roles (keys).

Now, SonataAdminBundle uses a particular way to check the keys in a context of admin class, just doing the following: $admin->isGranted('list'), this is because he has his own isGranted() function (where 'list' is the action name), but really what it does is build the role name (by using the current admin code) before check it, so he verify this finally: isGranted('ROLE_APP_BUNDLE_ADMIN_FOO_ADMIN_LIST') -this key it's what we need "give" to the user-.

How to get the role list from Sonata admin system?

In a controller context:

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

    // the sonata admin container
    $pool = $this->get('sonata.admin.pool');

    foreach ($pool->getAdminServiceIds() as $id) {
        // gets the registered admin instance from id service name
        $admin = $pool->getInstance($id);

        // the role security handler instance (must be configured)
        $securityHandler = $admin->getSecurityHandler();

        // gets the base role name from admin code 
        // e.g. 'ROLE_APP_BUNDLE_ADMIN_FOO_ADMIN_%s'
        $baseRole = $securityHandler->getBaseRole($admin);

        // gets the access actions (e.g. LIST, CREATE, EDIT, etc.)
        foreach (array_keys($admin->getSecurityInformation()) as $action) {
            // add the final role name
            // e.g. 'ROLE_APP_BUNDLE_ADMIN_FOO_ADMIN_LIST'
            $roles[] = sprintf($baseRole, $action);
        }
    }

    return $roles;
}

Next, you can do anything with that (e.g. create a custom form type to manage the user roles property). You could to sort, grouping these roles to show the user this list in the simplest possible way.

Up here, we can assign roles and work without using even the role_hierarchy.

More details http://symfony.com/doc/current/bundles/SonataAdminBundle/reference/security.html

1
votes

You can define a custom user permission Voter for your User entity, see here.

namespace AppBundle\Security;

use AppBundle\Entity\User;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface;

class UserVoter extends Voter
{

    private $decisionManager;

    public function __construct(AccessDecisionManagerInterface $decisionManager)
    {
        $this->decisionManager = $decisionManager;
    }

    protected function supports($attribute, $subject)
    {

        // only vote on User objects inside this voter
        if (!$subject instanceof User) {
            return false;
        }

        return true;
    }

    protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
    {
        // ROLE_SUPER_ADMIN can do anything! The power!
        if ($this->decisionManager->decide($token, array('ROLE_SUPER_ADMIN'))) {
            return true;
        }

        $user = $token->getUser();

        if (!$user instanceof User) {
            // the user must be logged in; if not, deny access
            return false;
        }

        /** @var User $targetUser */
        $targetUser = $subject;

        // Put your custom logic here
        switch ($attribute) {
            case "ROLE_SONATA_ADMIN_USER_VIEW":
                return true;
            case "ROLE_SONATA_ADMIN_USER_EDIT":
                return ($user === $targetUser);
        }
        return false;

    }
}

Then you create the service

sonata_admin.user_voter:
    class: AppBundle\Security\UserVoter
    arguments: ['@security.access.decision_manager']
    public: false
    tags:
        - { name: security.voter }

Be carefull of the access decision strategy, I may not work depending on your configuration if it's defined to unanimous or consensus

You may also add a direct link/route to the user's own edit page if you don't want to give every user access to the user list.

EDIT

To restrict user role edition, as you don't want a user to edit its own role, you can simply edit the configureFormFields function :

protected function configureFormFields(FormMapper $formMapper)
{
    $formMapper
        ->add('username')
        ->add('plainPassword', 'text', array(
                 'required' => false,
            )
        )
        /* your other fields */
    ;



    if ($this->isGranted('ROLE_SUPER_ADMIN')) {
        $formMapper->add('roles', \Symfony\Component\Form\Extension\Core\Type\CollectionType::class, array(
            'entry_type'   => \Symfony\Component\Form\Extension\Core\Type\ChoiceType::class,
            'entry_options'  => array(
                'choices'  => array(
                    "ROLE_OPTICKSB2B" => "ROLE_OPTICKSB2B",
                    "ROLE_ADMIN" => "ROLE_ADMIN",
                    "ROLE_SUPER_ADMIN" => "ROLE_SUPER_ADMIN"
                ),
            )
        ));
    }

    $formMapper
        ->add('isActive')
        ->add('title')
        ->add('firstname')
        ->add('lastname')
    ;
}

Obviously, Symfony forms component will check for you than no other field are added.