Symfony/Monolog: Custom processor with FOSUserBundle data - php

I'd like to add the userId and username, provided by FOSUserBundle, to my Logger (Monolog). I followed this tutorial to be able to log the IP, it's working.
My code below breaks, because $this->tokenStorage->getToken() is NULL, even if I'm logged in.
services.yml
monolog.processor.user:
class: AppBundle\Log\UserProcessor
arguments: [ "#request_stack", "#security.token_storage" ]
tags:
- { name: monolog.processor }
AppBundle/Log/UserProcessor.php
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use AppBundle\Entity\User;
class UserProcessor
{
private $requestStack;
private $tokenStorage;
public function __construct(RequestStack $requestStack, TokenStorageInterface $tokenStorage)
{
$this->requestStack = $requestStack;
$this->tokenStorage = $tokenStorage;
}
public function __invoke(array $record)
{
$username = '';
$userId = 0;
$user = $this->tokenStorage->getToken()->getUser();
// !!! ERROR !!!
// $this->tokenStorage->getToken() is NULL
if ($user instanceof User) {
$username = $user->getUsername();
$userId = $user->getId();
}
$record['extra']['user_id'] = $userId;
$record['extra']['username'] = $username;
return $record;
}
}
Any ideas?
Thanks in advance!

The problem is that the processor is called to log something before security has been able to act and get the current user. Just check if token exists and if it doesn´t don't log user

Related

How can I have optional services in the Symfony framework?

I would like to be able to get the current logged in user's credentials (email, password, etc) from the container. So, this is what I did:
security.token:
class: Symfony\Component\Security\Core\Authentication\Token\TokenInterface
factory: ["#security.token_storage", "getToken"]
private: true
security.current_user_credentials:
class: Symfony\Component\Security\Core\User\UserInterface
factory: ["#security.token", "getUser"]
security.current_user:
class: AppBundle\Entity\User
factory: ["#security.current_user_credentials", "getUser"]
When I do this and I'm logged in, it works fine. However, when I'm logged out, I get this in dev.log:
[2015-06-22 12:28:11] php.CRITICAL: Fatal Error: Call to a member function getUser() on string {"type":1,"file":"/var/www/html/phoenix/app/cache/dev/appDevDebugProjectContainer.php","line":3107,"level":-1,"stack":[{"function":"getSecurity_CurrentUserService","type":"->","class":"appDevDebugProjectContainer","file":"/var/www/html/phoenix/app/bootstrap.php.cache","line":2140,"args":[]},{"function":"get","type":"->","class":"Symfony\\Component\\DependencyInjection\\Container","file":"/var/www/html/phoenix/app/cache/dev/appDevDebugProjectContainer.php","line":674,"args":[]},{"function":"getCommandHistoryCreatorService","type":"->","class":"appDevDebugProjectContainer","file":"/var/www/html/phoenix/app/bootstrap.php.cache","line":2140,"args":[]},{"function":"get","type":"->","class":"Symfony\\Component\\DependencyInjection\\Container","file":"/var/www/html/phoenix/app/cache/dev/classes.php","line":1929,"args":[]},{"function":"lazyLoad","type":"->","class":"Symfony\\Component\\EventDispatcher\\ContainerAwareEventDispatcher","file":"/var/www/html/phoenix/app/cache/dev/classes.php","line":1894,"args":[]},{"function":"getListeners","type":"->","class":"Symfony\\Component\\EventDispatcher\\ContainerAwareEventDispatcher","file":"/var/www/html/phoenix/vendor/symfony/symfony/src/Symfony/Component/EventDispatcher/Debug/TraceableEventDispatcher.php","line":99,"args":[]},{"function":"getListeners","type":"->","class":"Symfony\\Component\\EventDispatcher\\Debug\\TraceableEventDispatcher","file":"/var/www/html/phoenix/vendor/symfony/symfony/src/Symfony/Component/EventDispatcher/Debug/TraceableEventDispatcher.php","line":158,"args":[]},{"function":"getNotCalledListeners","type":"->","class":"Symfony\\Component\\EventDispatcher\\Debug\\TraceableEventDispatcher","file":"/var/www/html/phoenix/vendor/symfony/symfony/src/Symfony/Component/HttpKernel/DataCollector/EventDataCollector.php","line":48,"args":[]},{"function":"lateCollect","type":"->","class":"Symfony\\Component\\HttpKernel\\DataCollector\\EventDataCollector","file":"/var/www/html/phoenix/vendor/symfony/symfony/src/Symfony/Component/HttpKernel/Profiler/Profiler.php","line":115,"args":[]},{"function":"saveProfile","type":"->","class":"Symfony\\Component\\HttpKernel\\Profiler\\Profiler","file":"/var/www/html/phoenix/vendor/symfony/symfony/src/Symfony/Component/HttpKernel/EventListener/ProfilerListener.php","line":146,"args":[]},{"function":"onKernelTerminate","type":"->","class":"Symfony\\Component\\HttpKernel\\EventListener\\ProfilerListener","file":"/var/www/html/phoenix/vendor/symfony/symfony/src/Symfony/Component/EventDispatcher/Debug/WrappedListener.php","line":61,"args":[]},{"function":"call_user_func:{/var/www/html/phoenix/vendor/symfony/symfony/src/Symfony/Component/EventDispatcher/Debug/WrappedListener.php:61}","file":"/var/www/html/phoenix/vendor/symfony/symfony/src/Symfony/Component/EventDispatcher/Debug/WrappedListener.php","line":61,"args":[]},{"function":"__invoke","type":"->","class":"Symfony\\Component\\EventDispatcher\\Debug\\WrappedListener","file":"/var/www/html/phoenix/app/cache/dev/classes.php","line":1824,"args":[]},{"function":"call_user_func:{/var/www/html/phoenix/app/cache/dev/classes.php:1824}","file":"/var/www/html/phoenix/app/cache/dev/classes.php","line":1824,"args":[]},{"function":"doDispatch","type":"->","class":"Symfony\\Component\\EventDispatcher\\EventDispatcher","file":"/var/www/html/phoenix/app/cache/dev/classes.php","line":1757,"args":[]},{"function":"dispatch","type":"->","class":"Symfony\\Component\\EventDispatcher\\EventDispatcher","file":"/var/www/html/phoenix/app/cache/dev/classes.php","line":1918,"args":[]},{"function":"dispatch","type":"->","class":"Symfony\\Component\\EventDispatcher\\ContainerAwareEventDispatcher","file":"/var/www/html/phoenix/vendor/symfony/symfony/src/Symfony/Component/EventDispatcher/Debug/TraceableEventDispatcher.php","line":124,"args":[]},{"function":"dispatch","type":"->","class":"Symfony\\Component\\EventDispatcher\\Debug\\TraceableEventDispatcher","file":"/var/www/html/phoenix/app/bootstrap.php.cache","line":3067,"args":[]},{"function":"terminate","type":"->","class":"Symfony\\Component\\HttpKernel\\HttpKernel","file":"/var/www/html/phoenix/app/bootstrap.php.cache","line":2409,"args":[]},{"function":"terminate","type":"->","class":"Symfony\\Component\\HttpKernel\\Kernel","file":"/var/www/html/phoenix/web/app_dev.php","line":20,"args":[]},{"function":"{main}","file":"/var/www/html/phoenix/web/app_dev.php","line":0,"args":[]}]} []
Is it possible to make the security.current_user_credentials and security.current_user optional? Is this error caused by these services?
Recently I ran into a similar issue and if you try to access a route that does not exist you might see the same error. I was working on a task where I needed to get hold of logged in user in my service and this is how I achieved it
My services.yml
services:
student_application_subscriber:
class: namespace\YourBundle\EventListener\StudentApplicationSubscriber
arguments:
- #doctrine.orm.entity_manager
- #security.token_storage
- #security.authorization_checker
- #twig
This is my service class StudentApplicationSubscriber
namespace yournamespace\YourBundleBundle\EventListener;
use Doctrine\ORM\EntityManager;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class StudentApplicationSubscriber implements EventSubscriberInterface
{
protected $em;
protected $twig;
protected $tokenStorage;
protected $authChecker;
function __construct(EntityManager $em, $tokenStorage, $authChecker, $twig)
{
$this->em = $em;
$this->twig = $twig;
$this->tokenStorage = $tokenStorage;
$this->authChecker = $authChecker;
}
public static function getSubscribedEvents()
{
return array(
'kernel.request' => 'onKernelRequest'
);
}
public function onKernelRequest()
{
if (!$token = $this->tokenStorage->getToken()) {
return;
}
$user = $token->getUser();
if (!is_object($user)) {
// there is no user - the user may not be logged in
return;
}
//get details of logged in user
$get_user_details = $this->tokenStorage->getToken()->getUser();
//make sure to pull information when user is logged in
if ($this->authChecker->isGranted('IS_AUTHENTICATED_FULLY')) {
//get user id of logged in user
$userId = $get_user_details->getId();
//perform your logic here
}
}
}
What are you trying to achieve?
At first sight I would suggest would be either having a kernel listener that would check if there is a User and performe the required actions, or check that in your security.current_user_credentials.
I guess, if you just pass to your service and add below logic inside that function, then it work for both annon and authenticated users:
function dummyFunction($securityContext)) {
$email = $username = '';
if($securityContext->isGranted('IS_AUTHENTICATED_FULLY')) {
$email = $securityContext->getToken()->getUser()->getEmail();
$username = $securityContext->getToken()->getUser()->getUsername();
}
..........................
}

FOSUserBundle - Force password change after first login

In a Symfony2 application using FOSUserBundle for user management, the user table has been filled through an import script from a csv file and the password generated from a combination of data.
I would like to force the user to change his password at the first login.
When the event FOSUserEvents::SECURITY_IMPLICIT_LOGIN occurs, redirect to the route fos_user_change_password if the field last_login is NULL.
My idea was rewriting the method onImplicitLogin(UserEvent $event) of the class AGI\UserBundle\EventListener\LastLoginListener like this but the class is not overwritten:
public function onImplicitLogin(UserEvent $event) {
$user = $event->getUser ();
if ($user->getLastLogin () === null) {
$user->setLastLogin ( new \DateTime () );
$this->userManager->updateUser ( $user );
$response = new RedirectResponse ( $this->router->generate ( 'fos_user_change_password' ) );
$this->session->getFlashBag ()->add ( 'notice', 'Please change your password' );
$event->setResponse ( $response );
}
}
I already have a bundle overwriting FOSUserBundle and it works for controllers, forms, etc but It looks like it is not the way to do it with eventListeners.
How can I force the user to change the password after the first login?
With the help of the precious hint from #sjagr about fos_user.security.implicit_login that drove me to fos_user.security.implicit_login and an external topic about doing stuff right after login, I got a working solution.
AGI\UserBundle\Resources\config\services.yml
login_listener:
class: 'AGI\UserBundle\EventListener\LoginListener'
arguments: ['#security.context', '#router', '#event_dispatcher']
tags:
- { name: 'kernel.event_listener', event: 'security.interactive_login', method: onSecurityInteractiveLogin }
AGI\UserBundle\EventListener\LoginListener.php
<?php
namespace AGI\UserBundle\EventListener;
use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Security\Core\SecurityContext;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Bundle\FrameworkBundle\Routing\Router;
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
class LoginListener {
private $securityContext;
private $router;
private $dispatcher;
public function __construct(SecurityContext $securityContext, Router $router, EventDispatcherInterface $dispatcher) {
$this->securityContext = $securityContext;
$this->router = $router;
$this->dispatcher = $dispatcher;
}
public function onSecurityInteractiveLogin(InteractiveLoginEvent $event) {
if ($this->securityContext->isGranted ( 'IS_AUTHENTICATED_FULLY' )) {
$user = $event->getAuthenticationToken ()->getUser ();
if ($user->getLastLogin () === null) {
$this->dispatcher->addListener ( KernelEvents::RESPONSE, array (
$this,
'onKernelResponse'
) );
}
}
}
public function onKernelResponse(FilterResponseEvent $event) {
$response = new RedirectResponse ( $this->router->generate ( 'fos_user_change_password' ) );
$event->setResponse ( $response );
}
}
Thank you
If you require user change password due to some business rules, you can use kernel request EventListener:
<?php
namespace App\EventListener;
use App\Model\UserInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
class ChangePasswordListener
{
private TokenStorageInterface $security;
private RouterInterface $router;
private array $excludedRoutes = [
'admin_change_password',
'admin_login',
'admin_login_check',
'admin_logout',
];
public function __construct(
TokenStorageInterface $security,
RouterInterface $router
) {
$this->security = $security;
$this->router = $router;
}
public function onKernelRequest(RequestEvent $event): void
{
if (false === $event->isMasterRequest()) {
return;
}
if ($event->getRequest()->isXmlHttpRequest()) {
return;
}
$currentRoute = $event->getRequest()->get('_route');
if (\in_array($currentRoute, $this->excludedRoutes, true)) {
return;
}
$token = $this->security->getToken();
if (null === $token) {
return;
}
$user = $token->getUser();
if ($user instanceof UserInterface && $user->shouldPasswordChange()) {
$response = new RedirectResponse($this->router->generate('admin_security_profile_change_password'));
$event->setResponse($response);
}
}
}
services.yaml:
services:
App\EventListener\ChangePasswordListener:
arguments:
- '#security.token_storage'
- '#router'
tags:
- { name: kernel.event_listener, event: kernel.request, method: onKernelRequest, priority: -100 }
You should provide also own UserInterface with method "shouldPasswordChange" and custom implementation of it.
It works great with Symfony 5.0 and PHP 7.4 but if you modify this code it should works also for lower PHP versions.

Anonymous user object in symfony

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.'; }
}

In FOSUserBundle, How to initially set user role on REGISTRATION_COMPLETED event?

I'm trying to give each successfully registered user a ROLE_USER role. I'm new to FOSUserBundle, So from what I've read in the documentation, It's done by hocking logic into the controllers.
Here's my NewUserGroupSet Event listener:
<?php
namespace Tsk\TstBundle\EventListener;
use Doctrine\ODM\MongoDB\DocumentManager;
use FOS\UserBundle\Doctrine\UserManager;
use FOS\UserBundle\Event\FilterUserResponseEvent;
use FOS\UserBundle\FOSUserEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class NewUserGroupSet implements EventSubscriberInterface
{
protected $um;
protected $dm;
public function __construct(UserManager $um, DocumentManager $dm)
{
$this->um = $um;
$this->dm = $dm;
}
public static function getSubscribedEvents()
{
return array(
FOSUserEvents::REGISTRATION_COMPLETED => "onRegistrationSuccess",
);
}
public function onRegistrationSuccess(FilterUserResponseEvent $event)
{
$user = $event->getUser();
$user->setRoles(array('ROLE_USER'));
$this->um->updateUser($user);
$this->dm->flush();
}
}
?>
And is registered as a service as follows:
parameters:
tsk_user.group_set.class: Tsk\TstBundle\EventListener\NewUserGroupSet
services:
tsk_user.group_set:
class: %tsk_user.group_set.class%
arguments: [#fos_user.user_manager, #doctrine.odm.mongodb.document_manager]
tags:
- { name: kernel.event_subscriber }
But when I register a new user, Nothing happens. No roles is being set whatsoever.
Any help would be appreciated.
Have you tried calling addRole() FOSUser entity function,if you notice the setRole function in entity it is looping through the array to roles and passing it to addRole
public function setRoles(array $roles)
{
$this->roles = array();
foreach ($roles as $role) {
$this->addRole($role);
}
return $this;
}
Try with addRole() for single role
public function onRegistrationSuccess(FilterUserResponseEvent $event)
{
$user = $event->getUser();
$user->addRole('ROLE_USER');
$this->um->updateUser($user);
$this->dm->flush();
}

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

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)

Categories