Skip to content

Instantly share code, notes, and snippets.

@wizhippo
Created May 6, 2020 13:16
Show Gist options
  • Save wizhippo/d0af6473ceb8956cc9c819e292a480d1 to your computer and use it in GitHub Desktop.
Save wizhippo/d0af6473ceb8956cc9c819e292a480d1 to your computer and use it in GitHub Desktop.
<?php
namespace AppBundle\Security\Http\Firewall;
use eZ\Publish\API\Repository\Repository;
use eZ\Publish\Core\MVC\Symfony\Event\InteractiveLoginEvent;
use eZ\Publish\Core\MVC\Symfony\MVCEvents;
use eZ\Publish\Core\MVC\Symfony\Security\UserWrapped;
use eZUser;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use Symfony\Component\Security\Core\Exception\AuthenticationCredentialsNotFoundException;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Role\SwitchUserRole;
use Symfony\Component\Security\Core\User\UserCheckerInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Http\Event\SwitchUserEvent;
use Symfony\Component\Security\Http\Firewall\ListenerInterface;
use Symfony\Component\Security\Http\SecurityEvents;
/**
* Class SwitchUserListener.
*/
class SwitchUserListener implements ListenerInterface
{
private $tokenStorage;
private $provider;
private $userChecker;
private $providerKey;
private $accessDecisionManager;
private $usernameParameter;
private $role;
private $logger;
private $dispatcher;
private $stateless;
/**
* @var Repository
*/
private $repository;
private $module;
private $function;
public function __construct(
TokenStorageInterface $tokenStorage,
UserProviderInterface $provider,
UserCheckerInterface $userChecker,
$providerKey,
AccessDecisionManagerInterface $accessDecisionManager,
LoggerInterface $logger = null,
$usernameParameter = '_switch_user',
$role = 'ROLE_ALLOWED_TO_SWITCH',
EventDispatcherInterface $dispatcher = null,
$stateless = false,
Repository $repository = null,
$module = null,
$function = null
) {
if (empty($providerKey)) {
throw new \InvalidArgumentException('$providerKey must not be empty.');
}
$this->tokenStorage = $tokenStorage;
$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->stateless = $stateless;
$this->repository = $repository;
$this->module = $module;
$this->function = $function;
}
/**
* Handles the switch to another user.
*
* @param GetResponseEvent $event A GetResponseEvent instance
*
* @throws \LogicException if switching to a user failed
*/
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);
$request->server->set('QUERY_STRING', http_build_query($request->query->all()));
$response = new RedirectResponse($request->getUri(), 302);
$event->setResponse($response);
}
/**
* Attempts to switch to another user.
*
* @param Request $request A Request instance
*
* @return TokenInterface|null The new TokenInterface if successfully switched, null otherwise
*
* @throws \LogicException
* @throws AccessDeniedException
*/
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;
}
throw new \LogicException(sprintf('You are already switched to "%s" user.', $token->getUsername()));
}
if ($this->module !== null && $token->getUser() instanceof UserWrapped) {
if (false === $this->repository->getPermissionResolver()->hasAccess($this->module, $this->function,
$token->getUser()->getAPIUser())
) {
throw new AccessDeniedException();
}
} else {
if (false === $this->accessDecisionManager->decide($token, [$this->role])) {
throw new AccessDeniedException();
}
}
$username = $request->get($this->usernameParameter);
if (null !== $this->logger) {
$this->logger->info('Attempting to switch to user.', ['username' => $username]);
}
$user = $this->provider->loadUserByUsername($username);
$this->userChecker->checkPostAuth($user);
$roles = $user->getRoles();
$roles[] = new SwitchUserRole('ROLE_PREVIOUS_ADMIN', $this->tokenStorage->getToken());
$token = new UsernamePasswordToken($user, $user->getPassword(), $this->providerKey, $roles);
if (!$user instanceof eZUser) {
$subLoginEvent = new InteractiveLoginEvent($request, $token);
$this->dispatcher->dispatch(MVCEvents::INTERACTIVE_LOGIN, $subLoginEvent);
if ($subLoginEvent->hasAPIUser()) {
$apiUser = $subLoginEvent->getAPIUser();
} else {
return $token;
}
$token = new UsernamePasswordToken(
new UserWrapped($user, $apiUser),
$token->getCredentials(),
$this->providerKey,
$token->getRoles()
);
}
if (null !== $this->dispatcher) {
$switchEvent = new SwitchUserEvent($request, $token->getUser());
$this->dispatcher->dispatch(SecurityEvents::SWITCH_USER, $switchEvent);
}
return $token;
}
/**
* Attempts to exit from an already switched user.
*
* @param Request $request A Request instance
*
* @return TokenInterface The original TokenInterface instance
*
* @throws AuthenticationCredentialsNotFoundException
*/
private function attemptExitUser(Request $request)
{
if (false === $original = $this->getOriginalToken($this->tokenStorage->getToken())) {
throw new AuthenticationCredentialsNotFoundException('Could not find original Token object.');
}
if (null !== $this->dispatcher) {
$user = $this->provider->refreshUser($original->getUser());
$switchEvent = new SwitchUserEvent($request, $user);
$this->dispatcher->dispatch(SecurityEvents::SWITCH_USER, $switchEvent);
}
return $original;
}
/**
* Gets the original Token from a switched one.
*
* @param TokenInterface $token A switched TokenInterface instance
*
* @return TokenInterface|false The original TokenInterface instance, false if the current TokenInterface is not
* switched
*/
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