Create a gist now

Instantly share code, notes, and snippets.

What would you like to do?
services:
security.authentication.switchuser_listener:
class: Rainbow\SecurityBundle\Security\Firewall\SwitchUserListener
public: false
abstract: true
arguments: [ "@security.context",
"",
"@security.user_checker",
"",
"@security.access.decision_manager",
"@?logger",
"_switch_user",
"ROLE_ALLOWED_TO_SWITCH",
"@?event_dispatcher",
"ROLE_SWITCHABLE"
]
tags:
- { name: monolog.logger, channel: security }
<?php
namespace Rainbow\SecurityBundle\Security\Firewall;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use Symfony\Component\Security\Core\SecurityContextInterface;
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\Exception\AuthenticationCredentialsNotFoundException;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Http\Event\SwitchUserEvent;
use Symfony\Component\Security\Http\Firewall\ListenerInterface;
use Symfony\Component\Security\Http\SecurityEvents;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
class SwitchUserListener implements ListenerInterface
{
private $securityContext;
private $provider;
private $userChecker;
private $providerKey;
private $accessDecisionManager;
private $usernameParameter;
private $role;
private $logger;
private $dispatcher;
private $switchableRole;
public function __construct(SecurityContextInterface $securityContext,
UserProviderInterface $provider,
UserCheckerInterface $userChecker,
$providerKey,
AccessDecisionManagerInterface $accessDecisionManager,
LoggerInterface $logger = null,
$usernameParameter = '_switch_user',
$role = 'ROLE_ALLOWED_TO_SWITCH',
EventDispatcherInterface $dispatcher = null,
$switchableRole = 'ROLE_SWITCHABLE')
{
if (empty($providerKey)) {
throw new \InvalidArgumentException('$providerKey must not be empty.');
}
$this->securityContext = $securityContext;
$this->provider = $provider;
$this->userChecker = $userChecker;
$this->providerKey = $providerKey;
$this->accessDecisionManager = $accessDecisionManager;
$this->usernameParameter = $usernameParameter;
$this->role = $role;
$this->logger = $logger;
$this->dispatcher = $dispatcher;
$this->switchableRole = $switchableRole;
}
public function handle(GetResponseEvent $event)
{
$request = $event->getRequest();
if (!$request->get($this->usernameParameter)) {
return;
}
if ('_exit' === $request->get($this->usernameParameter)) {
$this->securityContext->setToken($this->attemptExitUser($request));
} else {
try {
$this->securityContext->setToken($this->attemptSwitchUser($request));
} catch (AuthenticationException $e) {
throw new \LogicException(sprintf('Switch User failed: "%s"', $e->getMessage()));
}
}
$request->query->remove($this->usernameParameter);
$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->securityContext->getToken();
$originalToken = $this->getOriginalToken($token);
if (false !== $originalToken) {
if ($token->getUsername() === $request->get($this->usernameParameter)) {
return $token;
} else {
throw new \LogicException(
sprintf('You are already switched to "%s" user.', $token->getUsername())
);
}
}
if (false === $this->accessDecisionManager->decide($token, array($this->role))) {
throw new AccessDeniedException();
}
$username = $request->get($this->usernameParameter);
if (null !== $this->logger) {
$this->logger->info(sprintf('Attempt to switch to user "%s"', $username));
}
$user = $this->provider->loadUserByUsername($username);
$this->userChecker->checkPostAuth($user);
$roles = $user->getRoles();
$roles[] = new SwitchUserRole('ROLE_PREVIOUS_ADMIN', $this->securityContext->getToken());
$token = new UsernamePasswordToken(
$user,
$user->getPassword(),
$this->providerKey,
$roles
);
// START: THIS IS WHAT WE NEED TO ADD
// Do not allow access when the user we switch to has not the switchable role
if (false === $this->accessDecisionManager->decide($token, array($this->switchableRole))) {
throw new AccessDeniedException();
}
// END: THIS IS WHAT WE NEED TO ADD
if (null !== $this->dispatcher) {
$switchEvent = new SwitchUserEvent($request, $token->getUser());
$this->dispatcher->dispatch(SecurityEvents::SWITCH_USER, $switchEvent);
}
return $token;
}
private function attemptExitUser(Request $request)
{
if (false === $original = $this->getOriginalToken($this->securityContext->getToken())) {
throw new AuthenticationCredentialsNotFoundException(
'Could not find original Token object.'
);
}
if (null !== $this->dispatcher) {
$switchEvent = new SwitchUserEvent($request, $original->getUser());
$this->dispatcher->dispatch(SecurityEvents::SWITCH_USER, $switchEvent);
}
return $original;
}
private function getOriginalToken(TokenInterface $token)
{
foreach ($token->getRoles() as $role) {
if ($role instanceof SwitchUserRole) {
return $role->getSource();
}
}
return false;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment