How to use the AccessDecisionManager in Symfony2 for authorization of arbitrary users?

10,233

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.

Share:
10,233
Adrian Günter
Author by

Adrian Günter

PHP5 veteran living in the southeast US.

Updated on June 22, 2022

Comments

  • Adrian Günter
    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, yet isGranted('ROLE_ADMIN', $user) returns TRUE even if the user was just created and has not been assigned any roles other than the default ROLE_USER, as long as the currently logged in user is granted ROLE_ADMIN. This leads me to believe the 2nd argument to isGranted() is just ignored and that the Token provided to AccessDecisionManager->decide() by the SecurityContext is used instead.

    If this is a bug I'll submit a report, but maybe I'm still doing something wrong?

  • Adrian Günter
    Adrian Günter almost 12 years
    Thank 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
    Adrian Günter almost 12 years
    After 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 the AccessDecisionManager->decide() call originating in isGranted(), with the value being set to the SecurityContext's token property).
  • Adrian Günter
    Adrian Günter over 11 years
    This 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
    Venkat Kotra over 10 years
    Getting error within $grantedRole->getRole() in Symfony 2.3
  • dr.scre
    dr.scre about 10 years
    Role hierarchy is taken into account by RoleHierarchyVoter, it is built into Symfony security component
  • dr.scre
    dr.scre about 10 years
    This will check permissions for the currently authenticated user and not for $myUser.
  • Adrian Günter
    Adrian Günter almost 9 years
    It'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
    Pierre de LESPINAY almost 9 years
    This is the most elegant solution. Thanks
  • reda la
    reda la about 8 years
    I think we can't get 'security.access.decision_manager' directly from container because it's a private service ??
  • dr.scre
    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
    Murilo over 3 years
    @dr.scre wouldnt it be better to just inject the decision manager instead of getting it from the container directly?