Subida del módulo y tema de PrestaShop

This commit is contained in:
Kaloyan
2026-04-09 18:31:51 +02:00
parent 12c253296f
commit 16b3ff9424
39262 changed files with 7418797 additions and 0 deletions

View File

@@ -0,0 +1,34 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Http\Firewall;
use Symfony\Component\HttpKernel\Event\RequestEvent;
/**
* A base class for listeners that can tell whether they should authenticate incoming requests.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
abstract class AbstractListener implements FirewallListenerInterface
{
final public function __invoke(RequestEvent $event): void
{
if (false !== $this->supports($event->getRequest())) {
$this->authenticate($event);
}
}
public static function getPriority(): int
{
return 0; // Default
}
}

View File

@@ -0,0 +1,98 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Http\Firewall;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\Security\Core\Authentication\Token\NullToken;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface;
use Symfony\Component\Security\Core\Authorization\Voter\AuthenticatedVoter;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use Symfony\Component\Security\Http\AccessMapInterface;
use Symfony\Component\Security\Http\Event\LazyResponseEvent;
/**
* AccessListener enforces access control rules.
*
* @author Fabien Potencier <fabien@symfony.com>
*
* @final
*/
class AccessListener extends AbstractListener
{
private TokenStorageInterface $tokenStorage;
private AccessDecisionManagerInterface $accessDecisionManager;
private AccessMapInterface $map;
public function __construct(TokenStorageInterface $tokenStorage, AccessDecisionManagerInterface $accessDecisionManager, AccessMapInterface $map, bool $exceptionOnNoToken = false)
{
if (false !== $exceptionOnNoToken) {
throw new \LogicException(\sprintf('Argument $exceptionOnNoToken of "%s()" must be set to "false".', __METHOD__));
}
$this->tokenStorage = $tokenStorage;
$this->accessDecisionManager = $accessDecisionManager;
$this->map = $map;
}
public function supports(Request $request): ?bool
{
[$attributes] = $this->map->getPatterns($request);
$request->attributes->set('_access_control_attributes', $attributes);
if ($attributes && [AuthenticatedVoter::PUBLIC_ACCESS] !== $attributes) {
return true;
}
return null;
}
/**
* Handles access authorization.
*
* @throws AccessDeniedException
*/
public function authenticate(RequestEvent $event): void
{
$request = $event->getRequest();
$attributes = $request->attributes->get('_access_control_attributes');
$request->attributes->remove('_access_control_attributes');
if (!$attributes || (
[AuthenticatedVoter::PUBLIC_ACCESS] === $attributes && $event instanceof LazyResponseEvent
)) {
return;
}
$token = $this->tokenStorage->getToken() ?? new NullToken();
if (!$this->accessDecisionManager->decide($token, $attributes, $request, true)) {
throw $this->createAccessDeniedException($request, $attributes);
}
}
private function createAccessDeniedException(Request $request, array $attributes): AccessDeniedException
{
$exception = new AccessDeniedException();
$exception->setAttributes($attributes);
$exception->setSubject($request);
return $exception;
}
public static function getPriority(): int
{
return -255;
}
}

View File

@@ -0,0 +1,47 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Http\Firewall;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\Security\Http\Authentication\AuthenticatorManagerInterface;
/**
* Firewall authentication listener that delegates to the authenticator system.
*
* @author Wouter de Jong <wouter@wouterj.nl>
*/
class AuthenticatorManagerListener extends AbstractListener
{
private AuthenticatorManagerInterface $authenticatorManager;
public function __construct(AuthenticatorManagerInterface $authenticationManager)
{
$this->authenticatorManager = $authenticationManager;
}
public function supports(Request $request): ?bool
{
return $this->authenticatorManager->supports($request);
}
public function authenticate(RequestEvent $event): void
{
$request = $event->getRequest();
$response = $this->authenticatorManager->authenticateRequest($request);
if (null === $response) {
return;
}
$event->setResponse($response);
}
}

View File

@@ -0,0 +1,100 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Http\Firewall;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\Security\Http\AccessMapInterface;
/**
* ChannelListener switches the HTTP protocol based on the access control
* configuration.
*
* @author Fabien Potencier <fabien@symfony.com>
*
* @final
*/
class ChannelListener extends AbstractListener
{
private AccessMapInterface $map;
private ?LoggerInterface $logger;
private int $httpPort;
private int $httpsPort;
public function __construct(AccessMapInterface $map, ?LoggerInterface $logger = null, int $httpPort = 80, int $httpsPort = 443)
{
$this->map = $map;
$this->logger = $logger;
$this->httpPort = $httpPort;
$this->httpsPort = $httpsPort;
}
/**
* Handles channel management.
*/
public function supports(Request $request): ?bool
{
[, $channel] = $this->map->getPatterns($request);
if ('https' === $channel && !$request->isSecure()) {
if (null !== $this->logger) {
if ('https' === $request->headers->get('X-Forwarded-Proto')) {
$this->logger->info('Redirecting to HTTPS. ("X-Forwarded-Proto" header is set to "https" - did you set "trusted_proxies" correctly?)');
} elseif (str_contains($request->headers->get('Forwarded', ''), 'proto=https')) {
$this->logger->info('Redirecting to HTTPS. ("Forwarded" header is set to "proto=https" - did you set "trusted_proxies" correctly?)');
} else {
$this->logger->info('Redirecting to HTTPS.');
}
}
return true;
}
if ('http' === $channel && $request->isSecure()) {
$this->logger?->info('Redirecting to HTTP.');
return true;
}
return false;
}
public function authenticate(RequestEvent $event): void
{
$request = $event->getRequest();
$event->setResponse($this->createRedirectResponse($request));
}
private function createRedirectResponse(Request $request): RedirectResponse
{
$scheme = $request->isSecure() ? 'http' : 'https';
if ('http' === $scheme && 80 != $this->httpPort) {
$port = ':'.$this->httpPort;
} elseif ('https' === $scheme && 443 != $this->httpsPort) {
$port = ':'.$this->httpsPort;
} else {
$port = '';
}
$qs = $request->getQueryString();
if (null !== $qs) {
$qs = '?'.$qs;
}
$url = $scheme.'://'.$request->getHost().$port.$request->getBaseUrl().$request->getPathInfo().$qs;
return new RedirectResponse($url, 301);
}
}

View File

@@ -0,0 +1,337 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Http\Firewall;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\Event\ResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Security\Core\Authentication\AuthenticationTrustResolver;
use Symfony\Component\Security\Core\Authentication\AuthenticationTrustResolverInterface;
use Symfony\Component\Security\Core\Authentication\Token\AbstractToken;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Authentication\Token\SwitchUserToken;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use Symfony\Component\Security\Core\Exception\UserNotFoundException;
use Symfony\Component\Security\Core\User\EquatableInterface;
use Symfony\Component\Security\Core\User\LegacyPasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Http\Event\TokenDeauthenticatedEvent;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
/**
* ContextListener manages the SecurityContext persistence through a session.
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
*
* @final
*/
class ContextListener extends AbstractListener
{
private TokenStorageInterface $tokenStorage;
private string $sessionKey;
private ?LoggerInterface $logger;
private iterable $userProviders;
private ?EventDispatcherInterface $dispatcher;
private bool $registered = false;
private AuthenticationTrustResolverInterface $trustResolver;
private ?\Closure $sessionTrackerEnabler;
/**
* @param iterable<mixed, UserProviderInterface> $userProviders
*/
public function __construct(TokenStorageInterface $tokenStorage, iterable $userProviders, string $contextKey, ?LoggerInterface $logger = null, ?EventDispatcherInterface $dispatcher = null, ?AuthenticationTrustResolverInterface $trustResolver = null, ?callable $sessionTrackerEnabler = null)
{
if (empty($contextKey)) {
throw new \InvalidArgumentException('$contextKey must not be empty.');
}
$this->tokenStorage = $tokenStorage;
$this->userProviders = $userProviders;
$this->sessionKey = '_security_'.$contextKey;
$this->logger = $logger;
$this->dispatcher = $dispatcher;
$this->trustResolver = $trustResolver ?? new AuthenticationTrustResolver();
$this->sessionTrackerEnabler = null === $sessionTrackerEnabler ? null : $sessionTrackerEnabler(...);
}
public function supports(Request $request): ?bool
{
return null; // always run authenticate() lazily with lazy firewalls
}
/**
* Reads the Security Token from the session.
*/
public function authenticate(RequestEvent $event): void
{
if (!$this->registered && null !== $this->dispatcher && $event->isMainRequest()) {
$this->dispatcher->addListener(KernelEvents::RESPONSE, $this->onKernelResponse(...));
$this->registered = true;
}
$request = $event->getRequest();
$session = $request->hasPreviousSession() ? $request->getSession() : null;
$request->attributes->set('_security_firewall_run', $this->sessionKey);
if (null !== $session) {
$usageIndexValue = $session instanceof Session ? $usageIndexReference = &$session->getUsageIndex() : 0;
$usageIndexReference = \PHP_INT_MIN;
$sessionId = $request->cookies->all()[$session->getName()] ?? null;
$token = $session->get($this->sessionKey);
// sessionId = true is used in the tests
if ($this->sessionTrackerEnabler && \in_array($sessionId, [true, $session->getId()], true)) {
$usageIndexReference = $usageIndexValue;
} else {
$usageIndexReference = $usageIndexReference - \PHP_INT_MIN + $usageIndexValue;
}
}
if (null === $session || null === $token) {
if ($this->sessionTrackerEnabler) {
($this->sessionTrackerEnabler)();
}
$this->tokenStorage->setToken(null);
return;
}
$token = $this->safelyUnserialize($token);
$this->logger?->debug('Read existing security token from the session.', [
'key' => $this->sessionKey,
'token_class' => \is_object($token) ? $token::class : null,
]);
if ($token instanceof TokenInterface) {
if (!$token->getUser()) {
throw new \UnexpectedValueException(\sprintf('Cannot authenticate a "%s" token because it doesn\'t store a user.', $token::class));
}
$originalToken = $token;
$token = $this->refreshUser($token);
if (!$token) {
$this->logger?->debug('Token was deauthenticated after trying to refresh it.');
$this->dispatcher?->dispatch(new TokenDeauthenticatedEvent($originalToken, $request));
}
} elseif (null !== $token) {
$this->logger?->warning('Expected a security token from the session, got something else.', ['key' => $this->sessionKey, 'received' => $token]);
$token = null;
}
if ($this->sessionTrackerEnabler) {
($this->sessionTrackerEnabler)();
}
$this->tokenStorage->setToken($token);
}
/**
* Writes the security token into the session.
*/
public function onKernelResponse(ResponseEvent $event): void
{
if (!$event->isMainRequest()) {
return;
}
$request = $event->getRequest();
if (!$request->hasSession() || $request->attributes->get('_security_firewall_run') !== $this->sessionKey) {
return;
}
$this->dispatcher?->removeListener(KernelEvents::RESPONSE, $this->onKernelResponse(...));
$this->registered = false;
$session = $request->getSession();
$sessionId = $session->getId();
$usageIndexValue = $session instanceof Session ? $usageIndexReference = &$session->getUsageIndex() : null;
$usageIndexReference = \PHP_INT_MIN;
$token = $this->tokenStorage->getToken();
if (!$this->trustResolver->isAuthenticated($token)) {
if ($request->hasPreviousSession()) {
$session->remove($this->sessionKey);
}
} else {
$session->set($this->sessionKey, serialize($token));
$this->logger?->debug('Stored the security token in the session.', ['key' => $this->sessionKey]);
}
if ($this->sessionTrackerEnabler && $session->getId() === $sessionId) {
$usageIndexReference = $usageIndexValue;
} else {
$usageIndexReference = $usageIndexReference - \PHP_INT_MIN + $usageIndexValue;
}
}
/**
* Refreshes the user by reloading it from the user provider.
*
* @throws \RuntimeException
*/
protected function refreshUser(TokenInterface $token): ?TokenInterface
{
$user = $token->getUser();
$userNotFoundByProvider = false;
$userDeauthenticated = false;
$userClass = $user::class;
foreach ($this->userProviders as $provider) {
if (!$provider instanceof UserProviderInterface) {
throw new \InvalidArgumentException(\sprintf('User provider "%s" must implement "%s".', get_debug_type($provider), UserProviderInterface::class));
}
if (!$provider->supportsClass($userClass)) {
continue;
}
try {
$refreshedUser = $provider->refreshUser($user);
$newToken = clone $token;
$newToken->setUser($refreshedUser, false);
// tokens can be deauthenticated if the user has been changed.
if ($token instanceof AbstractToken && $this->hasUserChanged($user, $newToken)) {
$userDeauthenticated = true;
$this->logger?->debug('Cannot refresh token because user has changed.', ['username' => $refreshedUser->getUserIdentifier(), 'provider' => $provider::class]);
continue;
}
$token->setUser($refreshedUser);
if (null !== $this->logger) {
$context = ['provider' => $provider::class, 'username' => $refreshedUser->getUserIdentifier()];
if ($token instanceof SwitchUserToken) {
$originalToken = $token->getOriginalToken();
$context['impersonator_username'] = $originalToken->getUserIdentifier();
}
$this->logger->debug('User was reloaded from a user provider.', $context);
}
return $token;
} catch (UnsupportedUserException) {
// let's try the next user provider
} catch (UserNotFoundException $e) {
$this->logger?->warning('Username could not be found in the selected user provider.', ['username' => $e->getUserIdentifier(), 'provider' => $provider::class]);
$userNotFoundByProvider = true;
}
}
if ($userDeauthenticated) {
return null;
}
if ($userNotFoundByProvider) {
return null;
}
throw new \RuntimeException(\sprintf('There is no user provider for user "%s". Shouldn\'t the "supportsClass()" method of your user provider return true for this classname?', $userClass));
}
private function safelyUnserialize(string $serializedToken): mixed
{
$token = null;
$prevUnserializeHandler = ini_set('unserialize_callback_func', __CLASS__.'::handleUnserializeCallback');
$prevErrorHandler = set_error_handler(function ($type, $msg, $file, $line, $context = []) use (&$prevErrorHandler) {
if (__FILE__ === $file && !\in_array($type, [\E_DEPRECATED, \E_USER_DEPRECATED], true)) {
throw new \ErrorException($msg, 0x37313BC, $type, $file, $line);
}
return $prevErrorHandler ? $prevErrorHandler($type, $msg, $file, $line, $context) : false;
});
try {
$token = unserialize($serializedToken);
} catch (\ErrorException $e) {
if (0x37313BC !== $e->getCode()) {
throw $e;
}
$this->logger?->warning('Failed to unserialize the security token from the session.', ['key' => $this->sessionKey, 'received' => $serializedToken, 'exception' => $e]);
} finally {
restore_error_handler();
ini_set('unserialize_callback_func', $prevUnserializeHandler);
}
return $token;
}
private static function hasUserChanged(UserInterface $originalUser, TokenInterface $refreshedToken): bool
{
$refreshedUser = $refreshedToken->getUser();
if ($originalUser instanceof EquatableInterface) {
return !$originalUser->isEqualTo($refreshedUser);
}
if ($originalUser instanceof PasswordAuthenticatedUserInterface || $refreshedUser instanceof PasswordAuthenticatedUserInterface) {
if (!$originalUser instanceof PasswordAuthenticatedUserInterface || !$refreshedUser instanceof PasswordAuthenticatedUserInterface || $originalUser->getPassword() !== $refreshedUser->getPassword()) {
return true;
}
if ($originalUser instanceof LegacyPasswordAuthenticatedUserInterface xor $refreshedUser instanceof LegacyPasswordAuthenticatedUserInterface) {
return true;
}
if ($originalUser instanceof LegacyPasswordAuthenticatedUserInterface && $refreshedUser instanceof LegacyPasswordAuthenticatedUserInterface && $originalUser->getSalt() !== $refreshedUser->getSalt()) {
return true;
}
}
$userRoles = array_map('strval', (array) $refreshedUser->getRoles());
if ($refreshedToken instanceof SwitchUserToken) {
$userRoles[] = 'ROLE_PREVIOUS_ADMIN';
}
if (
\count($userRoles) !== \count($refreshedToken->getRoleNames())
|| \count($userRoles) !== \count(array_intersect($userRoles, $refreshedToken->getRoleNames()))
) {
return true;
}
if ($originalUser->getUserIdentifier() !== $refreshedUser->getUserIdentifier()) {
return true;
}
return false;
}
/**
* @internal
*/
public static function handleUnserializeCallback(string $class): never
{
throw new \ErrorException('Class not found: '.$class, 0x37313BC);
}
}

View File

@@ -0,0 +1,234 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Http\Firewall;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Security\Core\Authentication\AuthenticationTrustResolverInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use Symfony\Component\Security\Core\Exception\AccountStatusException;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Exception\InsufficientAuthenticationException;
use Symfony\Component\Security\Core\Exception\LazyResponseException;
use Symfony\Component\Security\Core\Exception\LogoutException;
use Symfony\Component\Security\Http\Authorization\AccessDeniedHandlerInterface;
use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface;
use Symfony\Component\Security\Http\EntryPoint\Exception\NotAnEntryPointException;
use Symfony\Component\Security\Http\HttpUtils;
use Symfony\Component\Security\Http\SecurityRequestAttributes;
use Symfony\Component\Security\Http\Util\TargetPathTrait;
/**
* ExceptionListener catches authentication exception and converts them to
* Response instances.
*
* @author Fabien Potencier <fabien@symfony.com>
*
* @final
*/
class ExceptionListener
{
use TargetPathTrait;
private TokenStorageInterface $tokenStorage;
private string $firewallName;
private ?AccessDeniedHandlerInterface $accessDeniedHandler;
private ?AuthenticationEntryPointInterface $authenticationEntryPoint;
private AuthenticationTrustResolverInterface $authenticationTrustResolver;
private ?string $errorPage;
private ?LoggerInterface $logger;
private HttpUtils $httpUtils;
private bool $stateless;
public function __construct(TokenStorageInterface $tokenStorage, AuthenticationTrustResolverInterface $trustResolver, HttpUtils $httpUtils, string $firewallName, ?AuthenticationEntryPointInterface $authenticationEntryPoint = null, ?string $errorPage = null, ?AccessDeniedHandlerInterface $accessDeniedHandler = null, ?LoggerInterface $logger = null, bool $stateless = false)
{
$this->tokenStorage = $tokenStorage;
$this->accessDeniedHandler = $accessDeniedHandler;
$this->httpUtils = $httpUtils;
$this->firewallName = $firewallName;
$this->authenticationEntryPoint = $authenticationEntryPoint;
$this->authenticationTrustResolver = $trustResolver;
$this->errorPage = $errorPage;
$this->logger = $logger;
$this->stateless = $stateless;
}
/**
* Registers a onKernelException listener to take care of security exceptions.
*/
public function register(EventDispatcherInterface $dispatcher): void
{
$dispatcher->addListener(KernelEvents::EXCEPTION, $this->onKernelException(...), 1);
}
/**
* Unregisters the dispatcher.
*/
public function unregister(EventDispatcherInterface $dispatcher): void
{
$dispatcher->removeListener(KernelEvents::EXCEPTION, $this->onKernelException(...));
}
/**
* Handles security related exceptions.
*/
public function onKernelException(ExceptionEvent $event): void
{
$exception = $event->getThrowable();
do {
if ($exception instanceof AuthenticationException) {
$this->handleAuthenticationException($event, $exception);
return;
}
if ($exception instanceof AccessDeniedException) {
$this->handleAccessDeniedException($event, $exception);
return;
}
if ($exception instanceof LazyResponseException) {
$event->setResponse($exception->getResponse());
return;
}
if ($exception instanceof LogoutException) {
$this->handleLogoutException($event, $exception);
return;
}
} while (null !== $exception = $exception->getPrevious());
}
private function handleAuthenticationException(ExceptionEvent $event, AuthenticationException $exception): void
{
$this->logger?->info('An AuthenticationException was thrown; redirecting to authentication entry point.', ['exception' => $exception]);
try {
$event->setResponse($this->startAuthentication($event->getRequest(), $exception));
$event->allowCustomResponseCode();
} catch (\Exception $e) {
$event->setThrowable($e);
}
}
private function handleAccessDeniedException(ExceptionEvent $event, AccessDeniedException $exception): void
{
$event->setThrowable(new AccessDeniedHttpException($exception->getMessage(), $exception));
$token = $this->tokenStorage->getToken();
if (!$this->authenticationTrustResolver->isFullFledged($token)) {
$this->logger?->debug('Access denied, the user is not fully authenticated; redirecting to authentication entry point.', ['exception' => $exception]);
try {
$insufficientAuthenticationException = new InsufficientAuthenticationException('Full authentication is required to access this resource.', 0, $exception);
if (null !== $token) {
$insufficientAuthenticationException->setToken($token);
}
$event->setResponse($this->startAuthentication($event->getRequest(), $insufficientAuthenticationException));
} catch (\Exception $e) {
$event->setThrowable($e);
}
return;
}
$this->logger?->debug('Access denied, the user is neither anonymous, nor remember-me.', ['exception' => $exception]);
try {
if (null !== $this->accessDeniedHandler) {
$response = $this->accessDeniedHandler->handle($event->getRequest(), $exception);
if ($response instanceof Response) {
$event->setResponse($response);
}
} elseif (null !== $this->errorPage) {
$subRequest = $this->httpUtils->createRequest($event->getRequest(), $this->errorPage);
$subRequest->attributes->set(SecurityRequestAttributes::ACCESS_DENIED_ERROR, $exception);
$event->setResponse($event->getKernel()->handle($subRequest, HttpKernelInterface::SUB_REQUEST, true));
$event->allowCustomResponseCode();
}
} catch (\Exception $e) {
$this->logger?->error('An exception was thrown when handling an AccessDeniedException.', ['exception' => $e]);
$event->setThrowable(new \RuntimeException('Exception thrown when handling an exception.', 0, $e));
}
}
private function handleLogoutException(ExceptionEvent $event, LogoutException $exception): void
{
$event->setThrowable(new AccessDeniedHttpException($exception->getMessage(), $exception));
$this->logger?->info('A LogoutException was thrown; wrapping with AccessDeniedHttpException', ['exception' => $exception]);
}
private function startAuthentication(Request $request, AuthenticationException $authException): Response
{
if (null === $this->authenticationEntryPoint) {
$this->throwUnauthorizedException($authException);
}
$this->logger?->debug('Calling Authentication entry point.', ['entry_point' => $this->authenticationEntryPoint]);
if (!$this->stateless) {
$this->setTargetPath($request);
}
if ($authException instanceof AccountStatusException) {
// remove the security token to prevent infinite redirect loops
$this->tokenStorage->setToken(null);
$this->logger?->info('The security token was removed due to an AccountStatusException.', ['exception' => $authException]);
}
try {
$response = $this->authenticationEntryPoint->start($request, $authException);
} catch (NotAnEntryPointException) {
$this->throwUnauthorizedException($authException);
}
if (!$response instanceof Response) {
$given = get_debug_type($response);
throw new \LogicException(\sprintf('The "%s::start()" method must return a Response object ("%s" returned).', get_debug_type($this->authenticationEntryPoint), $given));
}
return $response;
}
protected function setTargetPath(Request $request): void
{
// session isn't required when using HTTP basic authentication mechanism for example
if ($request->hasSession() && $request->isMethodSafe() && !$request->isXmlHttpRequest()) {
$this->saveTargetPath($request->getSession(), $this->firewallName, $request->getUri());
}
}
private function throwUnauthorizedException(AuthenticationException $authException): never
{
$this->logger?->notice(\sprintf('No Authentication entry point configured, returning a %s HTTP response. Configure "entry_point" on the firewall "%s" if you want to modify the response.', Response::HTTP_UNAUTHORIZED, $this->firewallName));
throw new HttpException(Response::HTTP_UNAUTHORIZED, $authException->getMessage(), $authException, [], $authException->getCode());
}
}

View File

@@ -0,0 +1,45 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Http\Firewall;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Event\RequestEvent;
/**
* Can be implemented by firewall listeners.
*
* @author Christian Scheb <me@christianscheb.de>
* @author Nicolas Grekas <p@tchwork.com>
* @author Robin Chalas <robin.chalas@gmail.com>
*/
interface FirewallListenerInterface
{
/**
* Tells whether the authenticate() method should be called or not depending on the incoming request.
*
* Returning null means authenticate() can be called lazily when accessing the token storage.
*/
public function supports(Request $request): ?bool;
/**
* Does whatever is required to authenticate the request, typically calling $event->setResponse() internally.
*
* @return void
*/
public function authenticate(RequestEvent $event);
/**
* Defines the priority of the listener.
* The higher the number, the earlier a listener is executed.
*/
public static function getPriority(): int;
}

View File

@@ -0,0 +1,111 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Http\Firewall;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Exception\LogoutException;
use Symfony\Component\Security\Csrf\CsrfToken;
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
use Symfony\Component\Security\Http\Event\LogoutEvent;
use Symfony\Component\Security\Http\HttpUtils;
use Symfony\Component\Security\Http\ParameterBagUtils;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
/**
* LogoutListener logout users.
*
* @author Fabien Potencier <fabien@symfony.com>
*
* @final
*/
class LogoutListener extends AbstractListener
{
private TokenStorageInterface $tokenStorage;
private array $options;
private HttpUtils $httpUtils;
private ?CsrfTokenManagerInterface $csrfTokenManager;
private EventDispatcherInterface $eventDispatcher;
/**
* @param array $options An array of options to process a logout attempt
*/
public function __construct(TokenStorageInterface $tokenStorage, HttpUtils $httpUtils, EventDispatcherInterface $eventDispatcher, array $options = [], ?CsrfTokenManagerInterface $csrfTokenManager = null)
{
$this->tokenStorage = $tokenStorage;
$this->httpUtils = $httpUtils;
$this->options = array_merge([
'csrf_parameter' => '_csrf_token',
'csrf_token_id' => 'logout',
'logout_path' => '/logout',
], $options);
$this->csrfTokenManager = $csrfTokenManager;
$this->eventDispatcher = $eventDispatcher;
}
public function supports(Request $request): ?bool
{
return $this->requiresLogout($request);
}
/**
* Performs the logout if requested.
*
* If a CsrfTokenManagerInterface instance is available, it will be used to
* validate the request.
*
* @throws LogoutException if the CSRF token is invalid
* @throws \RuntimeException if the LogoutEvent listener does not set a response
*/
public function authenticate(RequestEvent $event): void
{
$request = $event->getRequest();
if (null !== $this->csrfTokenManager) {
$csrfToken = ParameterBagUtils::getRequestParameterValue($request, $this->options['csrf_parameter']);
if (!\is_string($csrfToken) || false === $this->csrfTokenManager->isTokenValid(new CsrfToken($this->options['csrf_token_id'], $csrfToken))) {
throw new LogoutException('Invalid CSRF token.');
}
}
$logoutEvent = new LogoutEvent($request, $this->tokenStorage->getToken());
$this->eventDispatcher->dispatch($logoutEvent);
if (!$response = $logoutEvent->getResponse()) {
throw new \RuntimeException('No logout listener set the Response, make sure at least the DefaultLogoutListener is registered.');
}
$this->tokenStorage->setToken(null);
$event->setResponse($response);
}
/**
* Whether this request is asking for logout.
*
* The default implementation only processed requests to a specific path,
* but a subclass could change this to logout requests where
* certain parameters is present.
*/
protected function requiresLogout(Request $request): bool
{
return isset($this->options['logout_path']) && $this->httpUtils->checkRequestPath($request, $this->options['logout_path']);
}
public static function getPriority(): int
{
return -127;
}
}

View File

@@ -0,0 +1,229 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Security\Http\Firewall;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Authentication\Token\SwitchUserToken;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
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\User\UserCheckerInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Http\Event\SwitchUserEvent;
use Symfony\Component\Security\Http\SecurityEvents;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
/**
* SwitchUserListener allows a user to impersonate another one temporarily
* (like the Unix su command).
*
* @author Fabien Potencier <fabien@symfony.com>
*
* @final
*/
class SwitchUserListener extends AbstractListener
{
public const EXIT_VALUE = '_exit';
private TokenStorageInterface $tokenStorage;
private UserProviderInterface $provider;
private UserCheckerInterface $userChecker;
private string $firewallName;
private AccessDecisionManagerInterface $accessDecisionManager;
private string $usernameParameter;
private string $role;
private ?LoggerInterface $logger;
private ?EventDispatcherInterface $dispatcher;
private bool $stateless;
private ?UrlGeneratorInterface $urlGenerator;
private ?string $targetRoute;
public function __construct(TokenStorageInterface $tokenStorage, UserProviderInterface $provider, UserCheckerInterface $userChecker, string $firewallName, AccessDecisionManagerInterface $accessDecisionManager, ?LoggerInterface $logger = null, string $usernameParameter = '_switch_user', string $role = 'ROLE_ALLOWED_TO_SWITCH', ?EventDispatcherInterface $dispatcher = null, bool $stateless = false, ?UrlGeneratorInterface $urlGenerator = null, ?string $targetRoute = null)
{
if ('' === $firewallName) {
throw new \InvalidArgumentException('$firewallName must not be empty.');
}
$this->tokenStorage = $tokenStorage;
$this->provider = $provider;
$this->userChecker = $userChecker;
$this->firewallName = $firewallName;
$this->accessDecisionManager = $accessDecisionManager;
$this->usernameParameter = $usernameParameter;
$this->role = $role;
$this->logger = $logger;
$this->dispatcher = $dispatcher;
$this->stateless = $stateless;
$this->urlGenerator = $urlGenerator;
$this->targetRoute = $targetRoute;
}
public function supports(Request $request): ?bool
{
// usernames can be falsy
$username = $request->get($this->usernameParameter);
if (null === $username || '' === $username) {
$username = $request->headers->get($this->usernameParameter);
}
// if it's still "empty", nothing to do.
if (null === $username || '' === $username) {
return false;
}
$request->attributes->set('_switch_user_username', $username);
return true;
}
/**
* Handles the switch to another user.
*
* @throws \LogicException if switching to a user failed
*/
public function authenticate(RequestEvent $event): void
{
$request = $event->getRequest();
$username = $request->attributes->get('_switch_user_username');
$request->attributes->remove('_switch_user_username');
if (null === $this->tokenStorage->getToken()) {
throw new AuthenticationCredentialsNotFoundException('Could not find original Token object.');
}
if (self::EXIT_VALUE === $username) {
$this->attemptExitUser($request);
} else {
try {
$this->tokenStorage->setToken($this->attemptSwitchUser($request, $username));
} catch (AuthenticationException $e) {
// Generate 403 in any conditions to prevent user enumeration vulnerabilities
throw new AccessDeniedException('Switch User failed: '.$e->getMessage(), $e);
}
}
if (!$this->stateless) {
$request->query->remove($this->usernameParameter);
$request->server->set('QUERY_STRING', http_build_query($request->query->all(), '', '&'));
$response = new RedirectResponse($this->urlGenerator && $this->targetRoute && self::EXIT_VALUE !== $username ? $this->urlGenerator->generate($this->targetRoute) : $request->getUri(), 302);
$event->setResponse($response);
}
}
/**
* Attempts to switch to another user and returns the new token if successfully switched.
*
* @throws \LogicException
* @throws AccessDeniedException
*/
private function attemptSwitchUser(Request $request, string $username): ?TokenInterface
{
$token = $this->tokenStorage->getToken();
$originalToken = $this->getOriginalToken($token);
if (null !== $originalToken) {
if ($token->getUserIdentifier() === $username) {
return $token;
}
// User already switched, exit before seamlessly switching to another user
$token = $this->attemptExitUser($request);
}
$currentUsername = $token->getUserIdentifier();
$nonExistentUsername = '_'.hash('xxh128', random_bytes(8).$username);
// To protect against user enumeration via timing measurements
// we always load both successfully and unsuccessfully
try {
$user = $this->provider->loadUserByIdentifier($username);
try {
$this->provider->loadUserByIdentifier($nonExistentUsername);
} catch (\Exception) {
}
} catch (AuthenticationException $e) {
$this->provider->loadUserByIdentifier($currentUsername);
throw $e;
}
if (false === $this->accessDecisionManager->decide($token, [$this->role], $user)) {
$exception = new AccessDeniedException();
$exception->setAttributes($this->role);
throw $exception;
}
$this->logger?->info('Attempting to switch to user.', ['username' => $username]);
$this->userChecker->checkPostAuth($user);
$roles = $user->getRoles();
$roles[] = 'ROLE_PREVIOUS_ADMIN';
$originatedFromUri = str_replace('/&', '/?', preg_replace('#[&?]'.$this->usernameParameter.'=[^&]*#', '', $request->getRequestUri()));
$token = new SwitchUserToken($user, $this->firewallName, $roles, $token, $originatedFromUri);
if (null !== $this->dispatcher) {
$switchEvent = new SwitchUserEvent($request, $token->getUser(), $token);
$this->dispatcher->dispatch($switchEvent, SecurityEvents::SWITCH_USER);
// use the token from the event in case any listeners have replaced it.
$token = $switchEvent->getToken();
}
return $token;
}
/**
* Attempts to exit from an already switched user and returns the original token.
*
* @throws AuthenticationCredentialsNotFoundException
*/
private function attemptExitUser(Request $request): TokenInterface
{
if (null === ($currentToken = $this->tokenStorage->getToken()) || null === $original = $this->getOriginalToken($currentToken)) {
throw new AuthenticationCredentialsNotFoundException('Could not find original Token object.');
}
if (null !== $this->dispatcher && $original->getUser() instanceof UserInterface) {
$user = $this->provider->refreshUser($original->getUser());
$original->setUser($user);
$switchEvent = new SwitchUserEvent($request, $user, $original);
$this->dispatcher->dispatch($switchEvent, SecurityEvents::SWITCH_USER);
$original = $switchEvent->getToken();
}
$this->tokenStorage->setToken($original);
return $original;
}
private function getOriginalToken(TokenInterface $token): ?TokenInterface
{
if ($token instanceof SwitchUserToken) {
return $token->getOriginalToken();
}
return null;
}
}