The site I'm working on includes a search box in the navigation bar for superusers, so that they can select from an array of the regular users which passes the selected username on to Symfony's ?_switch_user=
feature to impersonate.
I am using jQuery to return the route of the current page and append the appropriate ?_switch_user=username
for the required user like this:
TWIG:
{% if is_granted('ROLE_PREVIOUS_ADMIN') %}
<form class="navbar-form navbar-left" role="search">
<div class="form-group">
<input type="text" id="search-names" class="form-control" placeholder="User name">
</div>
</form>
<li><a href="{{ path( app.request.get('_route'), {'_switch_user':'_exit'}) }}">~Return To Admin~<span class="sr-only">Return To Admin</span></a></li>
{% endif %}
jQuery:
$(function() {
$("#search-names").autocomplete({
source: "{{ path('usersearch') }}",
minLength: 2,
select: function (event, matched) {
console.log(matched)
window.location = window.location + '?_switch_user=' + matched.item.value
}
});
})
In the above, I have to include the ~Return To Admin~
link so that the superuser can 'logout' of each impersonation - otherwise Symfony returns an error saying that another switch user is already signed in.
It would be a lot more '(super)user-friendly' if they could switch from one regular user to another, without having to request ?_switch_user=_exit
each time (although I will still keep the button as it is needed for when they perform admin-only tasks)
Is there a simple way for achieving this? I found an article that suggests a work-around by creating a new listener "Making impersonating a user more friendly" (the 'second feature' in their article) however I'm not able to get this working and I'm wondering if it's due to structural differences in Symfony3?
I finally got up the free time to go ahead and adapt the code you linked to Symfony 3. The code below should make the Return to Admin work as described in the link as well as make it possible to switch directly to another user when already switched.
<?php
namespace AppBundle\Listener;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Core\User\UserCheckerInterface;
use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Role\SwitchUserRole;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationCredentialsNotFoundException;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Http\Event\SwitchUserEvent;
use Symfony\Component\Security\Http\SecurityEvents;
use Symfony\Component\Security\Http\Firewall\SwitchUserListener;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
class MySwitchUserListener extends SwitchUserListener
{
private $useOverrideUri = true;
public function handle(GetResponseEvent $event)
{
$request = $event->getRequest();
if( !$request->get( $this->usernameParameter ) )
return;
if('_exit' === $request->get( $this->usernameParameter ) )
$this->tokenStorage->setToken( $this->attemptExitUser( $request ) );
else{
try{
$this->tokenStorage->setToken( $this->attemptSwitchUser( $request ) );
}catch( AuthenticationException $e ){
throw new \LogicException(sprintf('Switch User failed: "%s"', $e->getMessage()));
}
}
$request->query->remove( $this->usernameParameter );
$overrideUri = $session->get( 'onSwitchURI', NULL );
if( $request->get( 'returnTo' ) ){
$session->set( 'onSwitchURI', $request->get( 'returnTo' ) );
$request->query->remove( 'returnTo' );
}
else
$session->remove( 'onSwitchURI' );
$request->server->set( 'QUERY_STRING', http_build_query( $request->query->all() ) );
$response = new RedirectResponse( $request->getUri(), 302 );
$event->setResponse( $response );
}
private function attemptSwitchUser(Request $request)
{
$token = $this->tokenStorage->getToken();
$originalToken = $this->getOriginalToken($token);
if (false !== $originalToken) {
if ($token->getUsername() === $request->get($this->usernameParameter)) {
return $token;
}
$token = $originalToken;
$this->useOverrideUri = false;
}
if (false === $this->accessDecisionManager->decide($token, array($this->role)))
throw new AccessDeniedException();
$username = $request->get($this->usernameParameter);
if(null !== $this->logger)
$this->logger->info('Attempting to switch to user.', array('username' => $username));
$user = $this->provider->loadUserByUsername($username);
$this->userChecker->checkPostAuth($user);
$roles = $user->getRoles();
$roles[] = new SwitchUserRole( 'ROLE_PREVIOUS_ADMIN', $token );
$token = new UsernamePasswordToken( $user, $user->getPassword(), $this->providerKey, $roles );
if (null !== $this->dispatcher) {
$switchEvent = new SwitchUserEvent($request, $token->getUser());
$this->dispatcher->dispatch(SecurityEvents::SWITCH_USER, $switchEvent);
}
return $token;
}
}