Reputation: 3182
I have an application where the administrator often acts on behalf of the regular users. The flow is as follows:
<a href="{{ path('_main_app', {'_switch_user': user.username}) }}">
At this point the admin page shows a button <a href="{{ path('_admin', {'_switch_user': '_exit'}) }}">Exit {{app.user.username}} </a>
However, the admin users often forget to click it and instead click on another non-admin user. When they do this they get a message You are already switched to User XXX. Obviously this is intended, but it is still not a great experience for the admin users.
What would be the recommended approach for allowing the admins to switch users in this way? I would think that calling {'_switch_user': '_exit'}
every time they reach the list of normal users would be good option (along with forcing this page never to be cached), but I can’t see a way to do that programmatically.
Another way might be to visit the URL {'_switch_user': '_exit'}
in Javascript, but that seems very unsatisfactory.
What would the recommended approach be?
Upvotes: 4
Views: 1018
Reputation: 14683
As Joe stated, Symfony's SwitchUserListener especially SwitchUserListener::attemptExitUser has all the information you need to programatically logout an impersonated user.
Here's the gist of it (to be used inside a controller):
$authorizationChecker = $this->get('security.authorization_checker');
if ($authorizationChecker->isGranted('ROLE_PREVIOUS_ADMIN')) {
// Code from Symfony\Component\Security\Http\Firewall\SwitchUserListener
$tokenStorage = $this->get('security.token_storage');
$originalToken = false;
if ($currentToken = $tokenStorage->getToken()) {
foreach ($currentToken->getRoles() as $role) {
if ($role instanceof SwitchUserRole) {
$originalToken = $role->getSource();
break;
}
}
}
if ($currentToken === null || $originalToken === false) {
throw new AuthenticationCredentialsNotFoundException('Could not find original Token object.');
}
if ($this->has('event_dispatcher') && $originalToken->getUser() instanceof UserInterface) {
$eventDispatcher = $this->get('event_dispatcher');
// Attention: Put your user provider here. I wasn't able to find a way to
// get the current firewall's user provider programatically.
$userProvider = $this->get('my.user_provider');
$originalUser = $userProvider->refreshUser($originalToken->getUser());
$switchEvent = new SwitchUserEvent($request, $originalUser);
$eventDispatcher->dispatch(SecurityEvents::SWITCH_USER, $switchEvent);
}
$tokenStorage->setToken($originalToken);
// Redirect to the current action so that the request's user is the original one.
return $this->redirect($request->getRequestUri());
}
Please note:
$this->get('my.user_provider')
with the user provider of your firewall. Unfortunately I wasn't able to find a way to get the current firewall's user programatically.Upvotes: 0
Reputation: 2436
You should take a look at the SwitchUserListener Class (Symfony\Component\Security\Http\Firewall\SwitchUserListener).
This is where the magic happens (and the "you are already switched to... message is created"). The problem is it doesn't give you really that much options to hook into it. Actually the only official one is the event after a successful switch.
But you are able to replace this Listener completely with your own implementation. Used that in my own project to implement ip restrictions for a user switch (acls didn't work or i got it wrong)
Simply create your own and replace it in your services.yml
security.authentication.switchuser_listener:
class: AppBundle\Services\Security\SwitchUserListener
public: false
abstract: true
arguments: ['@security.token_storage', '', '@security.user_checker', '', '@security.access.decision_manager', '@?logger', '_switch_user', 'ROLE_ALLOWED_TO_SWITCH', '@?event_dispatcher']
tags:
- { name: monolog.logger, channel: security }
Edit: You will need to copy the whole thing though since all the relevant functions are private. It's not pretty but i don't see any other way (except in your usecase maybe don't show the switch-link at all while the user is already switched.)
Upvotes: 2
Reputation: 319
If you want to force a response to not be cached, you can do this on the controller level. You can render a private response as follow:
$expire = new \DateTime('now');
$expire->sub(new \DateInterval('P1Y'));
$response = new Response();
$response->setPrivate();
$response->setExpires($expire);
$response->headers->add(array('Pragma' => 'no-cache'));
And then rendering your view with render method in your controller, yan can pass as third parameter you private response
Upvotes: 0