I have a user object that has a property 'enabled'. I want every action to first check if the user is enabled before continuing.
Right now I have solved it with a Controller that every other controller extends, but using the setContainer function to catch every Controller action feels really hacky.
class BaseController extends Controller{
public function setContainer(ContainerInterface $container = null)
{
$this->container = $container;
$user = $this->getUser();
// Redirect disabled users to a info page
if (!$user->isEnabled() && !$this instanceof InfoController) {
return $this->redirectToRoute('path_to_info');
}
}
I have tried building this using a before filter (http://symfony.com/doc/current/event_dispatcher/before_after_filters.html), but could not get the User object..any tips?
EDIT:
This is my solution:
namespace AppBundle\Security;
use AppBundle\Controller\AccessDeniedController;
use AppBundle\Controller\ConfirmController;
use Symfony\Bundle\FrameworkBundle\Routing\Router;
use Symfony\Bundle\TwigBundle\Controller\ExceptionController;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;
class UserEnabledListener
{
private $tokenStorage;
private $router;
public function __construct(TokenStorage $tokenStorage, Router $router)
{
$this->tokenStorage = $tokenStorage;
$this->router = $router;
}
public function onKernelController(FilterControllerEvent $event)
{
$controller = $event->getController();
/*
* $controller passed can be either a class or a Closure.
* This is not usual in Symfony but it may happen.
* If it is a class, it comes in array format
*/
if (!is_array($controller)) {
return;
}
$controller = $controller[0];
// Skip enabled check when:
// - we are already are the AccessDenied controller, or
// - user confirms e-mail and becomes enabled again, or
// - Twig throws error in template
if ($controller instanceof AccessDeniedController ||
$controller instanceof ConfirmController ||
$controller instanceof ExceptionController) {
return;
}
$user = $this->tokenStorage->getToken()->getUser();
// Show info page when user is disabled
if (!$user->isEnabled()) {
$redirectUrl = $this->router->generate('warning');
$event->setController(function() use ($redirectUrl) {
return new RedirectResponse($redirectUrl);
});
}
}
}
EDIT 2:
Ok so turns out checking for each controller manually is really bad, as you will miss Controllers from third party dependencies. I'm going to use the Security annotation and do further custom logic in a custom Exception controller or template etc.
You can use an event listener to listen for any new request.
You'll need to inject the user and then do your verification:
<service id="my_request_listener" class="Namespace\MyListener">
<tag name="kernel.event_listener" event="kernel.request" method="onKernelRequest" />
<argument type="service" id="security.token_storage" />
</service>
Edit: Here is a snippet to give an example
class MyRequestListener {
private $tokenStorage;
public function __construct(TokenStorage $tokenStorage)
{
$this->tokenStorage = $tokenStorage;
}
public function onKernelRequest(GetResponseEvent $event)
{
if (!$event->getRequest()->isMasterRequest()) {
// don't do anything if it's not the master request
return;
}
if ($this->tokenStorage->getToken()) {
$user = $this->tokenStorage->getToken()->getUser();
//do your verification here
}
}
In your case I would use the #Security annotation, which can be very flexible if you use the expression language.
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
/**
* #Security("user.isEnabled()")
*/
class EventController extends Controller
{
// ...
}
In the end it's only 1 line in each of your controller files, and it has the advantage of being very readable (a developer new to the project would know immediately what is going on without having to go and check the contents of a BaseController or any potential before filter...)
More documentation on this here.
You can override also getuser() function in your BaseController also.
/**
* Get a user from the Security Token Storage.
*
* #return mixed
*
* #throws \LogicException If SecurityBundle is not available
*
* #see TokenInterface::getUser()
*/
protected function getUser()
{
if (!$this->container->has('security.token_storage')) {
throw new \LogicException('The SecurityBundle is not registered in your application.');
}
if (null === $token = $this->container->get('security.token_storage')->getToken()) {
return;
}
if (!is_object($user = $token->getUser())) {
// e.g. anonymous authentication
return;
}
// Redirect disabled users to a info page
if (!$user->isEnabled() && !$this instanceof InfoController) {
return $this->redirectToRoute('path_to_info');
}
return $user;
}
Related
How can I fetch and render the uid of the FE User via a Viewhelper? The below is working via a Controller ... but not in a Viewhelper. Where is the difference? I'm using 7.6.11 and at the end I would like to have the uid of the FE User and the usergroup uid of him and further use it in the html of the extension and in general partials ...
/typo3conf/ext/extension/Classes/ViewHelpers/UserViewHelper.php
<?php
namespace Vendor\Extension\ViewHelpers;
class UserViewHelper extends \TYPO3\CMS\Fluid\Core\ViewHelper\AbstractViewHelper {
/**
* User Repository
*
* #var \TYPO3\CMS\Extbase\Domain\Repository\FrontendUserRepository
* #inject
*/
protected $userRepository;
/**
* #var \TYPO3\CMS\Extbase\Domain\Repository\FrontendUserGroupRepository
* #inject
*/
protected $frontendUserGroupRepository;
public function render() {
$userIDTest = $this->userRepository->findByUid($GLOBALS['TSFE']->fe_user->user['uid']);
$this->view->assign('userIDTest', $userIDTest);
}
}
List.html
<f:layout name="Default" />
<f:section name="main">
{userIDTest.uid}
</f:section>
As suggested by Dimitry I replaced
$this->view->assign('userIDTest', $userIDTest);
with
return $userIDTest;
And in List.html I have this:
{namespace custom=Vendor\Extension\ViewHelpers}
<f:layout name="Default" />
<f:section name="main">
<f:alias map="{user: '{custom:user()}'}">
{user.uid} {user.username}
</f:alias>
</f:section>
... and after clearing all Caches (FE/BE/Install) and deleting typo3temp ... now its working!
In 7.x and upwards ViewHelpers are compiled, resulting in the render method being called only once for compiling. Afterwards, only the static method renderStatic() is called. You could overwrite renderStatic, it will be called every time:
<?php
namespace Vendor\Extension\ViewHelpers;
use TYPO3\CMS\Fluid\Core\ViewHelper\AbstractViewHelper;
class UserIdViewHelper extends AbstractViewHelper
{
public function render()
{
return static::renderStatic(
[],
$this->renderChildrenClosure,
$this->renderingContext
);
}
public static function renderStatic(
array $arguments,
\Closure $renderChildrenClosure,
RenderingContextInterface $renderingContext
) {
$userData = $GLOBALS['TSFE']->fe_user->user;
return null !== $userData ? (int)$userData['uid'] : null;
}
}
If you need to use some service in your ViewHelper, things get more complicated, since dependency injection won't work with compiled ViewHelpers. You need to get an object manager, and fetch an instance of the service with the object manager.
This could look like this, assuming you would want to use the FrontendUserRepository as service, because you want to return the entire user object, not only the users uid:
<?php
namespace Vendor\Extension\ViewHelpers;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Domain\Repository\FrontendUserRepository;
use TYPO3\CMS\Extbase\Object\ObjectManager;
use TYPO3\CMS\Fluid\Core\ViewHelper\AbstractViewHelper;
class UserViewHelper extends AbstractViewHelper
{
/**
* #var FrontendUserRepository
*/
private static $frontendUserRepository = null;
public function render()
{
return static::renderStatic(
[],
$this->renderChildrenClosure,
$this->renderingContext
);
}
public static function renderStatic(
array $arguments,
\Closure $renderChildrenClosure,
RenderingContextInterface $renderingContext
) {
$userData = $GLOBALS['TSFE']->fe_user->user;
if (null === $userData) {
return null;
}
return static::getFrontendUserRepository()->findByUid((int)$userData['uid']);
}
private static function getFrontendUserRepository()
{
if (null === static::$frontendUserRepository) {
$objectManager = GeneralUtility::makeInstance(ObjectManager::class);
static::$frontendUserRepository = $objectManager->get(FrontendUserRepository::class);
}
return static::$frontendUserRepository;
}
}
Disclaimer: All the code is written without actually running it, thus there are bugs in it.
if you want to return the user or the uid of the user in the viewhelper, just return it.
instead of
$this->view->assign('userIDTest', $userIDTest);
do this
return $userIDTest;
In your fluid you can use the user variables in different ways. The easiest one is to use the "alias" viewhelper: https://fluidtypo3.org/viewhelpers/fluid/master/AliasViewHelper.html
<f:alias map="{user: '{namespace:user()}'}">
{user.uid} {user.username}
</f:alias>
Hi I have a event Listener in symfony2, That I have registered accordingly as well, it need to call at before of any function call of any controller in my module. But it is calling on Whole application, I mean every module. but I want it to called only when some one will open My Module only.
//My Event Listener
namespace Edu\AccountBundle\EventListener;
use Doctrine\ODM\MongoDB\DocumentManager;
use Symfony\Bundle\FrameworkBundle\Routing\Router;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
use Edu\AccountBundle\CommonFunctions\CommonFunctions;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Edu\AccountBundle\Controller\FinancialYearController;
/*
* Class:BeforeControllerListener
* #DESC:its a Listener which will execute at very first of any action of any controller in Account module (Act as a beforeFilter Symfony2)
* #param : #session,#route,#db
* #sunilrawat#indivar.com
* #09-07-2015
*/
class BeforeControllerListener
{
private $session;
private $router;
private $commonFunctions;
public function __construct(Session $session, Router $router, DocumentManager $dm)
{
$this->session = $session;
$this->router = $router;
$this->dm = $dm;
$this->commonFunctions = new CommonFunctions();
}
public function onKernelController(FilterControllerEvent $event)
{
$controller = $event->getController();
if (!is_array($controller)) {
return;
}
if (!$controller[0] instanceof FinancialYearController) {
if ($this->commonFunctions->checkFinancialYear($this->dm) !== 0 ) {
return;
}
$this->session->getFlashBag()->add('error', 'OOPS!, YOU MUST CREATE FINANCIAL YEAR TO MAKE ANY PROCESS IN ACCOUNTS!');
$redirectUrl= $this->router->generate('financialyear');
$event->setController(function() use ($redirectUrl) {
return new RedirectResponse($redirectUrl);
});
}
}
}
//Services.xml
<service id="edu.account.listener" class="Edu\AccountBundle\EventListener\BeforeControllerListener">
<argument type="service" id="session"/>
<argument type="service" id="router"/>
<argument type="service" id="doctrine_mongodb.odm.document_manager"/>
<tag name="kernel.event_listener" event="kernel.controller" method="onKernelController"/>
</service>
Now when the Method is calling corectly very begining of any action of any controller, but it is calling for every controller of whole project, Instead I want it to call only in My Particular Module in my application.
Please guide what is missing in this.
Thanks in advance
It's quite normal that a kernel.controller event listener is called for every controller, it's the check inside the event listener that matters and that allows you for an early return if the controller does not match.
Your check does seem wrong though. You probably want to return if the controller is not the class you expect:
public function onKernelController(FilterControllerEvent $event)
{
$controller = $event->getController();
if (!is_array($controller)) {
return;
}
if (!$controller[0] instanceof FinancialYearController) {
return;
}
if ($this->commonFunctions->checkFinancialYear($this->dm) !== 0 ) {
return;
}
$this->session->getFlashBag()->add('error', 'OOPS!, YOU MUST CREATE FINANCIAL YEAR TO MAKE ANY PROCESS IN ACCOUNTS!');
$redirectUrl= $this->router->generate('financialyear');
$event->setController(function() use ($redirectUrl) {
return new RedirectResponse($redirectUrl);
});
}
}
I am using Synfony2 with FOSUserBundle and I have a custom userChecker where I want to validate the host of the user (we have several hosts pointing to the same IP). My problem is that inside my custom userChecker I can't access REQUEST, thus not the HOST of the request.
This is my user checker code
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien#symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
//Override by Mattias
namespace BizTV\UserBundle\Controller;
//namespace Symfony\Component\Security\Core\User;
use Symfony\Component\Security\Core\Exception\CredentialsExpiredException;
use Symfony\Component\Security\Core\Exception\LockedException;
use Symfony\Component\Security\Core\Exception\DisabledException;
use Symfony\Component\Security\Core\Exception\AccountExpiredException;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserChecker as OriginalUserChecker;
use Symfony\Component\HttpFoundation\Request as Request; //ADDED BY MW
/**
* UserChecker checks the user account flags.
*
* #author Fabien Potencier <fabien#symfony.com>
*/
class UserCheckerNew extends OriginalUserChecker
{
/**
* {#inheritdoc}
*/
public function checkPreAuth(UserInterface $user)
{
/*
//Test for companylock...
if ( !$user->getCompany()->getActive() ) {
throw new LockedException('The company of this user is locked.', $user);
}
if ( $user->getLocked() ) {
throw new LockedException('The admin of this company has locked this user.', $user);
}
*/
if (!$user instanceof AdvancedUserInterface) {
return;
}
if (!$user->isCredentialsNonExpired()) {
throw new CredentialsExpiredException('User credentials have expired.', $user);
}
}
/**
* {#inheritdoc}
*/
public function checkPostAuth(UserInterface $user)
{
//Test for companylock...
if ( !$user->getCompany()->getActive() ) {
throw new LockedException('The company of this user is locked.');
}
if ( $user->getLocked() ) {
throw new LockedException('The admin of this company has locked this user.');
}
/*
Validate HOST here
*/
if (!$user instanceof AdvancedUserInterface) {
return;
}
if (!$user->isAccountNonLocked()) {
throw new LockedException('User account is locked.', $user);
}
if (!$user->isEnabled()) {
throw new DisabledException('User account is disabled.', $user);
}
if (!$user->isAccountNonExpired()) {
throw new AccountExpiredException('User account has expired.', $user);
}
}
}
In the checkPostAuth function I tried different things like passing the request
public function checkPostAuth(UserInterface $user, Request $request)
Error saying my override must conform to the original/interface.
Trying to get the request as in a controller
$this->container->get('request_stack')->getCurrentRequest();
or like this
$currentHost = $request->getHost();
or like this
$cont = $this->getContainer();
or like this
$request = $this->getRequest();
or like this
$request = $container->get('request');
Yet no luck =) I'm no Symfony2 guru, as you can tell, I'm shooting from the hip here =)
Added parameters to config.yml according to gp-sflover's answer, my config.yml now looks like this:
services:
security.user_checker:
class: BizTV\UserBundle\Controller\UserCheckerNew
arguments: [ "#request" ]
scope: request
public: true
The error delivered before scope:request was added to the config was:
Scope Widening Injection detected: The definition "security.user_checker" references the service "request" which belongs to a narrower scope. Generally, it is safer to either move "security.user_checker" to scope "request" or alternatively rely on the provider pattern by injecting the container itself, and requesting the service "request" each time it is needed. In rare, special cases however that might not be necessary, then you can set the reference to strict=false to get rid of this error.'
When adding scope: request a very similar error is returned
Scope Widening Injection detected: The definition "security.authentication.provider.dao.main" references the service "security.user_checker" which belongs to a narrower scope. Generally, it is safer to either move "security.authentication.provider.dao.main" to scope "request" or alternatively rely on the provider pattern by injecting the container itself, and requesting the service "security.user_checker" each time it is needed. In rare, special cases however that might not be necessary, then you can set the reference to strict=false to get rid of this error
Adding public: true doesn't seem to make a difference. Also, I don't know what this public stuff really means, perhaps a security issue? The word public is always scary =)
Instead of extend the "OriginalUserChecker" class you can override the security.user_checker service to be able to inject the request_stack as argument and then retrieve it in your UserChecker class like this simple example:
service.xml
// Symfony >=2.6
<service id="security.user_checker"
class="Your\Bundle\Path\ToYour\UserCheckerClass">
<argument type="service" id="request_stack"/>
</service>
// Symfony <2.6
<service id="security.user_checker"
class="Your\Bundle\Path\ToYour\UserCheckerClass">
<argument type="service" id="request" public="true" scope="request"/>
</service>
UserCheckerClass
use Symfony\Component\Security\Core\User\UserCheckerInterface;
// Symfony >=2.6
use Symfony\Component\HttpFoundation\RequestStack;
// Symfony <2.6
use Symfony\Component\HttpFoundation\Request;
class UserChecker implements UserCheckerInterface
{
private $request;
public function __construct(
// Symfony >=2.6
RequestStack $request
// Symfony <2.6
Request $request
) {
$this->request = $request;
}
public function checkPreAuth(UserInterface $user)
{
// your checks here
}
public function checkPostAuth(UserInterface $user)
{
// your checks here
}
}
I never did get the request injection to work. I did however get an injection of the entire service container to work.
This is how I ended up doing it, with guidance from gp_sflover (if you post an answer with this code I will check yours as the correct answer, I don't want to steal cred, just get the truth out there ;] )
services:
security.user_checker:
#class: BizTV\UserBundle\Controller\UserChecker
class: BizTV\UserBundle\Controller\UserCheckerNew
arguments: ["#service_container"]
namespace BizTV\UserBundle\Controller;
use Symfony\Component\Security\Core\User\UserCheckerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\Exception\CredentialsExpiredException;
use Symfony\Component\Security\Core\Exception\LockedException;
use Symfony\Component\Security\Core\Exception\DisabledException;
use Symfony\Component\Security\Core\Exception\AccountExpiredException;
class UserCheckerNew implements UserCheckerInterface
{
protected $container;
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}
public function checkPreAuth(UserInterface $user)
{
if (!$user instanceof AdvancedUserInterface) {
return;
}
if (!$user->isCredentialsNonExpired()) {
throw new CredentialsExpiredException('User credentials have expired.', $user);
}
}
/**
* {#inheritdoc}
*/
public function checkPostAuth(UserInterface $user)
{
//Validate HOST here, make it look as though account doesn't exist if on wrong host
$host = $this->container->get('request')->getHost();
if ($host != "localhost") { //bypass all checks when on localhost
$brandedHost = $user->getCompany()->getBrandedHost();
if ( $brandedHost == "" ) { //if unset assume main
$brandedHost = "login.mydomain.se";
}
if ( $host != $brandedHost ) {
throw new LockedException('Invalid username or password.'); //dot added for debug
}
}
// end of host validation
//Test for companylock...
if ( !$user->getCompany()->getActive() ) {
throw new LockedException('The company of this user is locked.');
}
if ( $user->getLocked() ) {
throw new LockedException('The admin of this company has locked this user.');
}
if (!$user instanceof AdvancedUserInterface) {
return;
}
if (!$user->isAccountNonLocked()) {
throw new LockedException('User account is locked.', $user);
}
if (!$user->isEnabled()) {
throw new DisabledException('User account is disabled.', $user);
}
if (!$user->isAccountNonExpired()) {
throw new AccountExpiredException('User account has expired.', $user);
}
}
}
I'm using the basic user login/logout system provided with Symfony and it works fine as long as people log in. In that case the $user object is always provided as needed.
The problem is then when logged out (or not lgged in yet) there is no user object. Is there a possibility to have (in that case) a default user object provided with my own default values?
Thanks for your suggestions
Because the solution mention above by #Chopchop (thanks anyway for your effort) didn't work here I wrote a little workaround.
I created a new class called myController which extends Controller. The only function i override is the getUser() function. There I implement it like this:
public function getUser()
{
$user = Controller::getUser();
if ( !is_object($user) )
{
$user = new \ACME\myBundle\Entity\User();
$user->setUserLASTNAME ('RaRa');
$user->setID (0);
// etc...
}
return $user;
}
This works fine for me now. The only problem is that you really have to be careful NOT to forget to replace Controller by myController in all your *Controller.php files. So, better suggestions still welcome.
Works in Symfony 3.3
Using the suggestion of #Sfblaauw, I came up with a solution that uses a CompilerPass.
AppBundle/AppBundle.php
class AppBundle extends Bundle
{
public function build(ContainerBuilder $container)
{
parent::build($container);
$container->addCompilerPass(new OverrideAnonymousUserCompilerPass());
}
}
OverrideAnonymousUserCompilerPass.php
class OverrideAnonymousCompilerPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
$definition = $container->getDefinition('security.authentication.listener.anonymous');
$definition->setClass(AnonymousAuthenticationListener::class);
}
}
AnonymousAuthenticationListener.php
class AnonymousAuthenticationListener implements ListenerInterface
{
private $tokenStorage;
private $secret;
private $authenticationManager;
private $logger;
public function __construct(TokenStorageInterface $tokenStorage, $secret, LoggerInterface $logger = null, AuthenticationManagerInterface $authenticationManager = null)
{
$this->tokenStorage = $tokenStorage;
$this->secret = $secret;
$this->authenticationManager = $authenticationManager;
$this->logger = $logger;
}
public function handle(GetResponseEvent $event)
{
if (null !== $this->tokenStorage->getToken()) {
return;
}
try {
// This is the important line:
$token = new AnonymousToken($this->secret, new AnonymousUser(), array());
if (null !== $this->authenticationManager) {
$token = $this->authenticationManager->authenticate($token);
}
$this->tokenStorage->setToken($token);
if (null !== $this->logger) {
$this->logger->info('Populated the TokenStorage with an anonymous Token.');
}
} catch (AuthenticationException $failed) {
if (null !== $this->logger) {
$this->logger->info('Anonymous authentication failed.', array('exception' => $failed));
}
}
}
}
This file is a copy of the AnonymousAuthenticationListener that comes with Symfony, but with the AnonymousToken constructor changed to pass in an AnonymousUser class instead of a string. In my case, AnonymousUser is a class that extends my User object, but you can implement it however you like.
These changes mean that {{ app.user }} in Twig and UserInterface injections in PHP will always return a User: you can use isinstance to tell if it's an AnonymousUser, or add a method isLoggedIn to your User class which returns true in User but false in AnonymousUser.
you can redirect the user not authenticated and force a fake login (to create a user ANONYMOUS)
and set it as well on logout
public function logoutAction(){
$em = $this->getDoctrine()->getManager();
$user = $em->getRepository('VendorBundle:User')->findByUserName('annonymous');
$session = $this->getRequest()->getSession();
$session->set('user', $user);
}
and if user is not set
public function checkLoginAction(){
if(!$session->get('user')){
$em = $this->getDoctrine()->getManager();
$user = $em->getRepository('VendorBundle:User')->findByUserName('annonymous');
$session = $this->getRequest()->getSession();
$session->set('user', $user);
}
//this->redirect('/');
}
in you security.yml
security:
firewalls:
main:
access_denied_url: /check_login/
access_control:
- { path: ^/$, role: ROLE_USER }
This is only an example i haven't tested (and will probably don't, since i don't get the purpose of doing this:) )
Using Symfony 2.6
Like Gordon says use the authentication listener to override the default anonymous user.
Now you can add the properties that you need to the anonymous user, in my case the language and the currency.
security.yml
parameters:
security.authentication.listener.anonymous.class: AppBundle\Security\Http\Firewall\AnonymousAuthenticationListener
AnonymousAuthenticationListener.php
namespace AppBundle\Security\Http\Firewall;
...
use AppBundle\Security\User\AnonymousUser;
class AnonymousAuthenticationListener implements ListenerInterface
{
...
public function handle(GetResponseEvent $event)
{
...
try {
$token = new AnonymousToken($this->key, new AnonymousUser(), array());
...
}
}
}
AnonymousUser.php
class AnonymousUser implements UserInterface
{
public function getUsername() { return 'anon.'; }
}
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?
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.
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();
}
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
}
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
}
}
}
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.
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.
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);
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)