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 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.
SonataUserBundle
?Answer: we need to do the same as SonataUserBundle
. (But let's simplify a little)
ROLE_
in Symfony flat: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-.
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
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With