16

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?

j0k
  • 22,600
  • 28
  • 79
  • 90
Adrian Günter
  • 903
  • 2
  • 11
  • 23

8 Answers8

17

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.

dr.scre
  • 2,287
  • 1
  • 22
  • 22
  • 2
    This is the most elegant solution. Thanks – Pierre de LESPINAY Jul 13 '15 at 15:28
  • 1
    I think we can't get 'security.access.decision_manager' directly from container because it's a private service ?? – reda la May 16 '16 at 10:50
  • 1
    @redalaanait You are right, accessing a private service is not a good thing. But you can use [service aliasing](http://symfony.com/doc/current/components/dependency_injection/advanced.html#aliasing), which allows you to access even private services. – dr.scre May 16 '16 at 14:03
  • @dr.scre wouldnt it be better to just inject the decision manager instead of getting it from the container directly? – Murilo Jan 05 '21 at 03:34
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
}
Pierre
  • 776
  • 6
  • 27
3

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();
}
Marcelo Rodovalho
  • 880
  • 1
  • 15
  • 26
2

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
        }
    }
}
Julien B.
  • 3,023
  • 2
  • 18
  • 33
1

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.

Adrian Günter
  • 903
  • 2
  • 11
  • 23
0

This looks like an issue with the:

abstract class AbstractToken implements TokenInterface

Look at the constructor. Looks like roles are created on instantiation and not queried at run time.

public function __construct(array $roles = array())
{
    $this->authenticated = false;
    $this->attributes = array();

    $this->roles = array();
    foreach ($roles as $role) {
        if (is_string($role)) {
            $role = new Role($role);
        } elseif (!$role instanceof RoleInterface) {
            throw new \InvalidArgumentException(sprintf('$roles must be an array of strings, or RoleInterface instances, but got %s.', gettype($role)));
        }

        $this->roles[] = $role;
    }
}

Hence, the roles cannot change after the token has been created. I think the option is to write your own voter. I'm still looking around.

Dave Clemmer
  • 3,741
  • 12
  • 49
  • 72
  • 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. – Adrian Günter Jan 03 '13 at 07:01
  • Role hierarchy is taken into account by `RoleHierarchyVoter`, it is built into Symfony security component – dr.scre Mar 13 '14 at 13:55
0

Create a service AccessDecisionMaker (used Shady's solution)

<?php
namespace Bp\CommonBundle\Service;

use Symfony\Component\DependencyInjection\Container;
use Symfony\Component\Security\Core\Role\RoleInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Core\SecurityContext;

class AccessDecisionMaker
{
    /** @var Container */
    private $container;

    /** @var  SecurityContext */
    private $securityContext;

    function __construct($container)
    {
        $this->container = $container;

        if (!$this->securityContext) {
            // Ensure security context is created only once
            $this->securityContext = new SecurityContext($this->container->get(
                'security.authentication.manager'
            ), $this->container->get('security.access.decision_manager'));
        }
    }

    public function isGranted($roleToCheck, UserInterface $user)
    {
        if (!is_string($roleToCheck)) {
            if (!($roleToCheck instanceof RoleInterface)) {
                throw new \InvalidArgumentException('First argument expects a string or instance of RoleInterface');
            }
            $roleToCheck = $roleToCheck->getRole();
        }

        $token = new UsernamePasswordToken($user, null, $this->container->getParameter(
            'fos_user.firewall_name'
        ), $user->getRoles());
        $this->securityContext->setToken($token);
        if ($this->securityContext->isGranted($roleToCheck)) {
            return true;
        }

        return false;
    }

}

Configure this as a service

bp.access_decision_maker:
    class: Bp\CommonBundle\Service\AccessDecisionMaker
    arguments:  [@service_container ]

Use it

$this->container->get('bp.access_decision_maker')->isGranted("ROLE_ADMIN",$user);
Venkat Kotra
  • 10,413
  • 3
  • 49
  • 53
-3

You can use the security.context service with the isGranted method.

You can pass a second argument which is your object (see here).

In a controller:

$this->get('security.context')->isGranted('ROLE_FOOBAR', $myUser)
Olivier Dolbeau
  • 1,204
  • 9
  • 11
  • 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 Jul 02 '12 at 17:56
  • 2
    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 Jul 02 '12 at 18:34
  • 4
    This will check permissions for the *currently authenticated user* and not for $myUser. – dr.scre Mar 13 '14 at 13:57
  • 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. :) – Adrian Günter Jun 08 '15 at 00:34