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

8

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?

Doublespace answered 2/7, 2012 at 5:43 Comment(0)
I
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();
}
Indelicacy answered 10/3, 2016 at 2:13 Comment(0)
G
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.

Germanium answered 13/3, 2014 at 13:53 Comment(3)
I think we can't get 'security.access.decision_manager' directly from container because it's a private service ??Replevy
@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.Germanium
@Germanium wouldnt it be better to just inject the decision manager instead of getting it from the container directly?Girder
U
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
}
Unarmed answered 11/6, 2013 at 15:2 Comment(0)
I
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();
}
Indelicacy answered 10/3, 2016 at 2:13 Comment(0)
R
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
        }
    }
}
Ripping answered 19/12, 2020 at 17:47 Comment(1)
Yes, this is the most relevant answer as of Symfony5.Periwinkle
D
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.

Doublespace answered 3/7, 2012 at 6:19 Comment(1)
Getting error within $grantedRole->getRole() in Symfony 2.3Crore
K
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.

Kink answered 3/1, 2013 at 2:38 Comment(2)
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.Doradorado
Role hierarchy is taken into account by RoleHierarchyVoter, it is built into Symfony security componentGermanium
C
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);
Crore answered 30/11, 2013 at 20:18 Comment(0)
P
-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)
Pitchford answered 2/7, 2012 at 7:1 Comment(4)
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.Doradorado
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).Doradorado
This will check permissions for the currently authenticated user and not for $myUser.Germanium
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. @Germanium — nothing against you, nor your comment, by the way. :)Doradorado

© 2022 - 2024 — McMap. All rights reserved.