How to a dynamically add roles to a user? - symfony

When a user logs in, whether it be for the first time or via a cookie that's present, I want to assign them one or more additional user roles based on the result of some queries I run.
I think I need to create an Event Listener that gets the security component and entity manager injected, so I can run my queries and add roles to the current user.
I'm not quite sure if this is possible with events though, since the event would need to be fired within the Firewall context before the authorization is done, but after authentication.
I did see this existing question, but I can't get it to work (the roles are not actually recognized after the event is run).
Is it possible to do this with an event listener?
I think my alternative would be to use a postload lifecycle callback on the User entity and run some queries there, but that doesn't seem right.

You could create an event listener on the kernel. It will run everytime a page is loaded.
It will check if their is a logged in user and then you can do some custom logic to see if you need to update their role, if you do then update it log them in with the new settings and then they'll continue in the system with their new role.
I haven't tested this code, so it might have some bugs.
services.yml
bundle.eventlistener.roles:
class: Sample\MyBundle\EventListener\Roles
arguments: [#service_container, #security.context]
tags:
- { name: kernel.event_listener, event: kernel.controller, method: onKernelController }
Listener\Roles.php
namespace Sample\MyBundle\EventListener;
use Symfony\Component\DependencyInjection\ContainerInterface as Container;
use Symfony\Component\Security\Core\SecurityContext;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Sample\MyBundle\Entity\Users; //Your USER entity
class Roles {
private $container;
private $context;
public function __construct(Container $container, SecurityContext $context) {
$this->container = $container;
$this->context = $context;
}
public function onKernelController(FilterControllerEvent $event) {
if($this->context->getToken()->getUser() instanceof Users) {
//Custom logic to see if you need to update the role or not.
$user = $this->context->getToken()->getUser();
//Update your roles
$user->setRole('ROLE_WHATEVER');
$em = $this->container->get('doctrine')->getManager();
$em->persist($user);
$em->flush();
//Create new user token
//main == firewall setting
$token = new UsernamePasswordToken($user, $user->getPassword(), 'main', $user->getRoles());
$this->context->setToken($token);
}
}
}

Related

Custom decision manager authorisation in Symfony 4

I have a specific authorisation system in my application (asked by my managers). It is based on Joomla. Users are attached to usergroups. Every action (i.e page) in my application are resources and for each resources I have set an access level. I have then to compare the resource access level with the usergroups of the current user to grant access or not to this specific resource.
All those informations are stored in database which are in return entities in Symfony :
User <- ManyToMany -> Usergroups
Menu (all resources with path and access level)
I thought about the Voter system. It is kind alike of what I would want, I think. Can I hijack the support function for this ?
protected function supports($user, $resource)
{
//get usergroups of the $user => $usergroups
//get the access level of the resource => $resource_access
// if the attribute isn't one we support, return false
if (!in_array($usergroups, $resource_access)) {
return false;
}
return true;
}
The get the usergroups and the access level of the resource I will have to do some queries in the database. To use this, then I would to use the denyAccessUnlessGranted() function in all my controller (seems redundant by the way) ?
Do you think it would work or there is another system more suited for this case ? I thought of doing the control in a listener to the kernel.request event too.
Hope I am clear enough, I'm new to symfony and still have some issues to understand how everything are related and working.
The voter component should be a good fit for this, as its a passive approach that lets you implement any logic in a way where its fixable through code, without modifying any database specific acl tree not managed by symfony itself.
Voters are called if you use denyAccessUnlessGranted() or isGranted() either through code, annotation or twig.
Lets take a look at how you want to check if the current user has access to view the index page:
class SomeController {
public function index() {
$this->denyAccessUnlessGranted('VIEW', '/index');
// or use some magic method to replace '/index' with wathever you require,
// like injecting $request->getUri(), just make sure your voter can
// parse it quickly.
// ...
}
}
Now build the a very simple voter:
class ViewPageVoter extends Voter
{
/**
* #var EntityManagerInterface
*/
private $em;
public function __construct(EntityManagerInterface $em) {
$this->em = $em;
}
protected function supports($attribute, $subject)
{
return is_string($subject) && substr($subject, 0, 1) === '/';
}
protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
{
$currentUser = $token->getUser();
if(!$currentUser) {
// no user or authentication, deny
return false;
}
// Do the query to see if the user is allowed to view the resource.
// $this->em->getRepository(...) or
// $this->em->getConnection()
//
// $attribute = VIEW
// $subject = '/index'
// $currentUser = authenticated user
// return TRUE if allowed, return FALSE if not.
}
}
As a nice bonus you can easily see additional details on security voters in the /_profiler of that request, also indicating their respective vote on the subject.

PostPersist & PostFlush infinite loop in Symfony

I'm trying to link a coffee profile to a user after the user is in the database.
This coffee profile data is in a session and I'm using this session in the postFlush.
However This code is creating an infinite loop and I don't know why:
UserListener.php:
<?php
namespace AppBundle\EventListener;
use FOS\UserBundle\FOSUserEvents;
use FOS\UserBundle\Event\FormEvent;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\HttpFoundation\Session\Session;
use AppBundle\Entity\Consumption;
use AppBundle\Entity\CoffeeOption;
use AppBundle\Entity\Consumeable;
use AppBundle\Entity\MomentPreference;
use Doctrine\ORM\Event\LifecycleEventArgs;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Doctrine\ORM\Event\PostFlushEventArgs;
class UserListener
{
private $container;
private $user;
public function __construct(ContainerInterface $container = null)
{
$this->container = $container;
}
public function postPersist(LifecycleEventArgs $args)
{
$user = $args->getEntity();
$this->user = $user;
}
public function postFlush(PostFlushEventArgs $args)
{
$session = new Session();
if($session) {
$em = $args->getEntityManager();
$us = $em->getRepository('AppBundle:User')->findOneById($this->user->getId());
$consumption = $session->get('consumption');
$coffee = $session->get('coffee');
$moment = $session->get('moment');
$consumption->setUser($us);
//dummy data for the day, later this needs to be turned into datetime
$moment->setDay('monday');
$moment->setConsumption($consumption);
$em->persist($consumption);
$em->persist($coffee);
$em->persist($moment);
$em->flush();
} else {
return $this->redirectToRoute('fos_user_registration_register');
}
}
}
Services.yml:
zpadmin.listener.user:
class: AppBundle\EventListener\UserListener
arguments: ['#service_container']
tags:
- { name: doctrine.event_listener, event: postPersist }
- { name: doctrine.event_listener, event: postFlush }
What is causing this loop and how can I fix it?
In your postFlush event, you're flushing again. That's what causing the infinite loop because the postFlush event is triggered every time you're calling the flush method.
I'm not sure what you're trying to achieve but your goal is to create a coffee consumption everytime you're saving a user, you can add a test like this one at the start of your method:
$entity = $args->getObject();
if (!$entity instanceof User) {
return;
}
This will prevent the infinite loop.
And a few more things:
Your postPersist method seems useless. It'll be called every time an object is persisted so your $this->user propety will not necessarily be a User object.
If you need the user, you don't have to fetch it in your database. Simply use $args->getObject() to get the flushed entity. In addition with the test above, you'll be sure the method will return you a User object.
This is not a very good practice to check if the user is logged in in your Doctrine listener. This is not what the class is supposed to do.
Don't inject the container in your constructor. Inject only what you need (in this case... nothingĀ ?)
You are calling $em->flush() inside your postPersist event, in the docs it is stated that:
postFlush is called at the end of EntityManager#flush().
EntityManager#flush() can NOT be called safely inside its listeners.
You should use other events like prePersist or postPersist.
If possible try to avoid multiple flush() on a single request.
by the way, there is no need to do this, because your user object is already contained inside your $user variable.
$us =
$em->getRepository('AppBundle:User')->findOneById($this->user->getId());

How to impersonate user by id instead of username in symfony?

I can't figure out how to impersonate a user by user's id instead of user's username in Symfony?
The following trick which works with username can't work with id, as symfony is looking for username:
?_switch_user={id}
This is impossible to do without implementing your own firewall listener, as behind the scenes it loads the user from the userprovider (which only has a loadUserByUsername() method in its interface).
You could however implement your own firewall listener and get inspired by having a look at the code in Symfony\Component\Security\Http\Firewall\SwitchUserListener. For detailed information on implementing your own authentication provider, check the cookbook article.
EDIT:
One possible solution might be registering an extra request listener:
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
class LookupSwitchUserListener implements EventSubscriberInterface
{
private $repository;
public function __construct(UserRepository $repository)
{
$this->repository = $repository;
}
public static function getSubscribedEvents()
{
return [
KernelEvents::REQUEST => ['lookup', 12] // before the firewall
];
}
public function lookup(GetResponseEvent $event)
{
$request = $event->getRequest();
if ($request->has('_switch_user') {
return; // do nothing if already a _switch_user param present
}
if (!$id = $request->query->has('_switch_user_by_id')) {
return; // do nothing if no _switch_user_by_id param
}
// lookup $username by $id using the repository here
$request->attributes->set('_switch_user', $username);
}
}
Now register this listener in the service container:
services:
my_listener:
class: LookupSwitchUserListener
tags:
- { name: kernel.event_subscriber }
Calling a url with the ?_switch_user_by_id=xxx parameter should now correctly look up the username and set it so the SwitchUserListener can switch to the specified user.

symfony : best way to set a user with a relationship

I'm very new in development and symfony. I wonder me what's the best way to code this below.
I've 2 entities (user and account). There is a relation between them (create an account requiers a user).
I wonder me what is the best way to set the user in account entity (prepersist, controller, __construct) when I'm adding an new account ?
PREPERSIST
First, I didn't find anything to set the user with prepersit method. Is there a way ?
Something like that :
/**
* #ORM\PrePersist
*/
public function prePersist()
{
$this->user = $this->get('security.context')->getToken()->getUser();
$this->updatedAt = new \Datetime("now");
$this->isActive = false;
}
CONTROLLER
...
$user = new User();
$account = new Account();
$account->setUser($user);
...
CONSTRUCTOR
/* Entity account */
...
public function __construct($user)
{
$this->user = $user;
}
...
/* Controller account */
...
$account = new Account($this->get('security.context')->getToken()->getUser())
...
Hope you can help me.
Based on your code above, you don't need to hook into a doctrine event to accomplish what you want. You can create the association in the controller before persisting the Account object.
If you are using the Symfony security component, obtaining the user in the controller is as simple as $this->getUser(). You can inject User via the Account constructor method __construct($user) or a setter method setUser($user). Although the constructor method is more efficient, either way is correct.
And to persist the Account object to your database from within the controller:
$em = $this->getDoctrine()->getManager();
$em->persist($account);
$em->flush();
I would recommend creating Doctrine2 Listener / Subscriber and register it as a service, than listen to prePersist event of Account entity. You can inject any other needed services in listeners / subscribers.
All information you need can be found on: http://symfony.com/doc/current/cookbook/doctrine/event_listeners_subscribers.html

FOSUserBundle: Get EntityManager instance overriding Form Handler

I am starting with Symfony2 and I am trying to override FOS\UserBundle\Form\Handler\RegistrationFormHandler of FOSUserBundle.
My code is:
<?php
namespace Testing\CoreBundle\Form\Handler;
use FOS\UserBundle\Model\UserInterface;
use FOS\UserBundle\Form\Handler\RegistrationFormHandler as BaseHandler;
use Testing\CoreBundle\Entity\User as UserDetails;
class RegistrationFormHandler extends BaseHandler
{
protected function onSuccess(UserInterface $user, $confirmation)
{
// I need an instance of Entity Manager but I don't know where get it!
$em = $this->container->get('doctrine')->getEntityManager();
// or something like: $em = $this->getDoctrine()->getEntityManager
$userDetails = new UserDetails;
$em->persist($userDetails);
$user->setId($userDetails->getId());
parent::onSuccess($user, $confirmation);
}
}
So, the point is that I need an instance of Doctrine's Entity Manager but I don't know where/how get it in this case!
Any idea?
Thanks in advance!
You should not use EntityManager directly in most of the cases. Use a proper manager/provider service instead.
In case of FOSUserBundle service implementing UserManagerInterface is such a manager. It is accessible through fos_user.user_manager key in the service container (which is an allias to fos_user.user_manager.default). Of course registration form handler uses that service, it is accessible through userManager property.
You should not treat your domain-model (i.a. Doctrine's entities) as if it was exact representation of the database-model. This means, that you should assign objects to other objects (not their ids).
Doctrine is capable of handling nested objects within your entities (UserDetails and User objects have a direct relationship). Eventually you will have to configure cascade options for User entity.
Finally, UserDetails seems to be a mandatory dependency for each User. Therefore you should override UserManagerInterface::createUser() not the form handler - you are not dealing with user's details there anyway.
Create your own UserManagerInterface implementation:
class MyUserManager extends \FOS\UserBundle\Entity\UserManager {
/**
* {#inheritdoc}
*/
public function createUser() {
$user = parent::createUser();
$user->setUserDetails(new UserDetails());
// some optional code required for a proper
// initialization of User/UserDetails object
// that might require access to other objects
// not available inside the entity
return $user;
}
}
Register your own manager as a serive inside DIC:
<service id="my_project.user_manager" class="\MyProject\UserManager" parent="fos_user.user_manager.default" />
Configure FOSUserBundle to use your own implementation:
# /app/config/config.yml
fos_user:
...
service:
user_manager: my_project.user_manager

Resources