How to use the AccessDecisionManager in Symfony2 for authorization of arbitrary users?
Solution 1
security.context
Is deprecated since 2.6.
Use AuthorizationChecker
:
$token = new UsernamePasswordToken(
$user,
null,
'secured_area',
$user->getRoles()
);
$tokenStorage = $this->container->get('security.token_storage');
$tokenStorage->setToken($token);
$authorizationChecker = new AuthorizationChecker(
$tokenStorage,
$this->container->get('security.authentication.manager'),
$this->container->get('security.access.decision_manager')
);
if (!$authorizationChecker->isGranted('ROLE_ADMIN')) {
throw new AccessDeniedException();
}
Solution 2
You need only AccessDecisionManager
for this, no need for security context since you don't need authentication.
$user = new Core\Model\User();
$token = new UsernamePasswordToken($user, 'none', 'none', $user->getRoles());
$isGranted = $this->get('security.access.decision_manager')
->decide($token, array('ROLE_ADMIN'));
This will correctly take role hierarchy into account, since RoleHierarchyVoter
is registered by default
Update
As noted by @redalaanait, security.access.decision_manager is a private service, so accessing it directly is not a good thing to do. It's better to use service aliasing, which allows you to access private services.
Solution 3
Maybe you can instantiate a new securityContext instance and use it to check if user is granted :
$securityContext = new \Symfony\Component\Security\Core\SecurityContext($this->get('security.authentication.manager'), $this->get('security.access.decision_manager'));
$token = new \Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken($user, null, $this->container->getParameter('fos_user.firewall_name'), $user->getRoles());
$securityContext->setToken($token);
if ($securityContext->isGranted('ROLE_ADMIN')) {
// some stuff to do
}
Solution 4
I know this post is quite old, but I faced that problem recently and I created a service based on @dr.scre answer.
Here's how I did in Symfony 5.
<?php
declare(strict_types=1);
namespace App\Service;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface;
use Symfony\Component\Security\Core\User\UserInterface;
final class AccessDecisionMaker
{
private AccessDecisionManagerInterface $accessDecisionManager;
public function __construct(AccessDecisionManagerInterface $accessDecisionManager)
{
$this->accessDecisionManager = $accessDecisionManager;
}
public function isGranted(UserInterface $user, string $role): bool
{
$token = new UsernamePasswordToken($user, 'none', 'none', $user->getRoles());
return $this->accessDecisionManager->decide($token, [$role]);
}
}
Now I can use it wherever I want.
<?php
declare(strict_types=1);
namespace App\Service;
use App\Entity\User;
use Symfony\Component\Security\Core\Security;
class myClass
{
private Security $security;
private AccessDecisionMaker $decisionMaker;
public function __construct(Security $security, AccessDecisionMaker $decisionMaker)
{
$this->security = $security;
$this->decisionMaker = $decisionMaker;
}
public function someMethod(?User $user): void
{
$user = $user ?: $this->security->getUser();
if ($this->decisionMaker->isGranted($user, 'ROLE_SOME_ROLE')) {
// do something
} else {
// do something else
}
}
}
Solution 5
RoleVoter
disregards the $object passed through from SecurityContext->isGranted()
. This results in the RoleHierarchyVoter
extracting roles from the Token
instead of a provided UserInterface
$object (if exists), so I had to find a different route.
Maybe there is a better way to go about this and if there is I'd sure like to know, but this is the solution I came up with:
First I implemented ContainerAwareInterface
in my User class so I could access the security component from within it:
final class User implements AdvancedUserInterface, ContainerAwareInterface
{
// ...
/**
* @var ContainerInterface
*/
private $container;
// ...
public function setContainer(ContainerInterface $container = null)
{
if (null === $container) {
throw new \Exception('First argument to User->setContainer() must be an instance of ContainerInterface');
}
$this->container = $container;
}
// ...
}
Then I defined a hasRole()
method:
/**
* @param string|\Symfony\Component\Security\Core\Role\RoleInterface $roleToCheck
* @return bool
* @throws \InvalidArgumentException
*/
public function hasRole($roleToCheck)
{
if (!is_string($roleToCheck)) {
if (!($roleToCheck instanceof \Symfony\Component\Security\Core\Role\RoleInterface)) {
throw new \InvalidArgumentException('First argument expects a string or instance of RoleInterface');
}
$roleToCheck = $roleToCheck->getRole();
}
/**
* @var \Symfony\Component\Security\Core\SecurityContext $thisSecurityContext
*/
$thisSecurityContext = $this->container->get('security.context');
$clientUser = $thisSecurityContext->getToken()->getUser();
// determine if we're checking a role on the currently authenticated client user
if ($this->equals($clientUser)) {
// we are, so use the AccessDecisionManager and voter system instead
return $thisSecurityContext->isGranted($roleToCheck);
}
/**
* @var \Symfony\Component\Security\Core\Role\RoleHierarchy $thisRoleHierarchy
*/
$thisRoleHierarchy = $this->container->get('security.role_hierarchy');
$grantedRoles = $thisRoleHierarchy->getReachableRoles($this->getRoles());
foreach ($grantedRoles as $grantedRole) {
if ($roleToCheck === $grantedRole->getRole()) {
return TRUE;
}
}
return FALSE;
}
From a controller:
$user = new User();
$user->setContainer($this->container);
var_dump($user->hasRole('ROLE_ADMIN'));
var_dump($this->get('security.context')->isGranted('ROLE_ADMIN'));
var_dump($this->get('security.context')->isGranted('ROLE_ADMIN', $user));
$user->addUserSecurityRole('ROLE_ADMIN');
var_dump($user->hasRole('ROLE_ADMIN'));
Output:
boolean false
boolean true
boolean true
boolean true
Although it does not involve the AccessDecisionManager
or registered voters (unless the instance being tested is the currently authenticated user), it is sufficient for my needs as I just need to ascertain whether or not a given user has a particular role.
Comments
-
Adrian Günter almost 2 years
I'd like to be able to verify whether or not attributes (roles) are granted to any arbitrary object implementing
UserInterface
in Symfony2. Is this possible?UserInterface->getRoles()
is not suitable for my needs because it does not take the role hierarchy into account, and I'd rather not reinvent the wheel in that department, which is why I'd like to use the Access Decision Manager if possible.Thanks.
In response to Olivier's solution below, here is my experience:
You can use the security.context service with the isGranted method. You can pass a second argument which is your object.
$user = new Core\Model\User(); var_dump($user->getRoles(), $this->get('security.context')->isGranted('ROLE_ADMIN', $user));
Output:
array (size=1) 0 => string 'ROLE_USER' (length=9) boolean true
My role hierarchy:
role_hierarchy: ROLE_USER: ~ ROLE_VERIFIED_USER: [ROLE_USER] ROLE_ADMIN: [ROLE_VERIFIED_USER] ROLE_SUPERADMIN: [ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH] ROLE_ALLOWED_TO_SWITCH: ~
My
UserInterface->getRoles()
method:public function getRoles() { $roles = [$this->isVerified() ? 'ROLE_VERIFIED_USER' : 'ROLE_USER']; /** * @var UserSecurityRole $userSecurityRole */ foreach ($this->getUserSecurityRoles() as $userSecurityRole) { $roles[] = $userSecurityRole->getRole(); } return $roles; }
ROLE_ADMIN
must be explicitly assigned, yetisGranted('ROLE_ADMIN', $user)
returnsTRUE
even if the user was just created and has not been assigned any roles other than the defaultROLE_USER
, as long as the currently logged in user is grantedROLE_ADMIN
. This leads me to believe the 2nd argument toisGranted()
is just ignored and that theToken
provided toAccessDecisionManager->decide()
by theSecurityContext
is used instead.If this is a bug I'll submit a report, but maybe I'm still doing something wrong?
-
Adrian Günter almost 12 yearsThank you, but this was one of the first methods I attempted and it does not appear to work. It still decides based upon the current user's token from what I can tell. I have edited my original post and provided the result of your suggestion on my setup.
-
Adrian Günter almost 12 yearsAfter tracing the execution of
SecurityContext->isGranted()
, it appears$object
is never considered in the voting process.RoleVoter->vote()
accepts$object
as an argument, but the variable is not used in the method body at all and roles are instead extracted from the$token
argument (passed through from theAccessDecisionManager->decide()
call originating inisGranted()
, with the value being set to theSecurityContext
'stoken
property). -
Adrian Günter over 11 yearsThis still does not explain why Symfony disregards the role hierarchy and the 2nd argument to
SecurityContext->isGranted
. It looks like the implementation is possibly incomplete? Either way I don't think the solution is registering another voter. I've since revised my posted solution a bit and moved it into a service from the model, and it has been working well. I'll post the updated solution soon. -
Venkat Kotra over 10 yearsGetting error within
$grantedRole->getRole()
in Symfony 2.3 -
dr.scre about 10 yearsRole hierarchy is taken into account by
RoleHierarchyVoter
, it is built into Symfony security component -
dr.scre about 10 yearsThis will check permissions for the currently authenticated user and not for $myUser.
-
Adrian Günter almost 9 yearsIt's odd to me that my in-depth analysis of why this, something that to me seems extremely counterintuitive, is true seems to be undeserving of the same upvotes that are given for a boiled down comment stating the end result. Not to mention that I literally said the same thing, worded a bit differently, in my first comment 2 years prior. @dr.scre — nothing against you, nor your comment, by the way. :)
-
Pierre de LESPINAY almost 9 yearsThis is the most elegant solution. Thanks
-
reda la about 8 yearsI think we can't get 'security.access.decision_manager' directly from container because it's a private service ??
-
dr.scre about 8 years@redalaanait You are right, accessing a private service is not a good thing. But you can use service aliasing, which allows you to access even private services.
-
Murilo over 3 years@dr.scre wouldnt it be better to just inject the decision manager instead of getting it from the container directly?