diff --git a/composer.json b/composer.json index 4214b086..55b77290 100644 --- a/composer.json +++ b/composer.json @@ -37,7 +37,8 @@ "symfony/http-kernel": "^4.4.20||^5.0.11||^6.0", "symfony/polyfill-php80": "^1.22", "symfony/psr-http-message-bridge": "^1.2||^2.0", - "symfony/security-core": "^4.4.20||^5.0.11||^6.0" + "symfony/security-core": "^4.4.20||^5.0.11||^6.0", + "symfony/security-http": "^4.4.20||^5.0.11||^6.0" }, "require-dev": { "doctrine/dbal": "^2.13||^3.0", diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 0dfd27df..c04293a5 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -160,6 +160,11 @@ parameters: count: 1 path: src/EventListener/ConsoleCommandListener.php + - + message: "#^Call to an undefined method Symfony\\\\Component\\\\HttpKernel\\\\Event\\\\KernelEvent\\:\\:isMasterRequest\\(\\)\\.$#" + count: 1 + path: src/EventListener/LoginListener.php + - message: "#^Call to an undefined method Symfony\\\\Component\\\\HttpKernel\\\\Event\\\\KernelEvent\\:\\:isMasterRequest\\(\\)\\.$#" count: 1 @@ -313,27 +318,32 @@ parameters: - message: "#^Call to function method_exists\\(\\) with \\$this\\(Sentry\\\\SentryBundle\\\\Tests\\\\EventListener\\\\AuthenticatedTokenStub\\) and 'setAuthenticated' will always evaluate to false\\.$#" count: 1 - path: tests/EventListener/RequestListenerTest.php + path: tests/EventListener/LoginListenerTest.php - message: "#^Parameter \\#1 \\$user of method Symfony\\\\Component\\\\Security\\\\Core\\\\Authentication\\\\Token\\\\AbstractToken\\:\\:setUser\\(\\) expects Symfony\\\\Component\\\\Security\\\\Core\\\\User\\\\UserInterface, string\\|Stringable\\|Symfony\\\\Component\\\\Security\\\\Core\\\\User\\\\UserInterface given\\.$#" count: 1 - path: tests/EventListener/RequestListenerTest.php + path: tests/EventListener/LoginListenerTest.php + + - + message: "#^Parameter \\#2 \\$firewallName of class Symfony\\\\Component\\\\Security\\\\Core\\\\Authentication\\\\Token\\\\SwitchUserToken constructor expects string, null given\\.$#" + count: 1 + path: tests/EventListener/LoginListenerTest.php - message: "#^Parameter \\#3 \\$roles of class Symfony\\\\Component\\\\Security\\\\Core\\\\Authentication\\\\Token\\\\SwitchUserToken constructor expects array\\, string given\\.$#" count: 1 - path: tests/EventListener/RequestListenerTest.php + path: tests/EventListener/LoginListenerTest.php - message: "#^Parameter \\#4 \\$originalToken of class Symfony\\\\Component\\\\Security\\\\Core\\\\Authentication\\\\Token\\\\SwitchUserToken constructor expects Symfony\\\\Component\\\\Security\\\\Core\\\\Authentication\\\\Token\\\\TokenInterface, array\\ given\\.$#" count: 1 - path: tests/EventListener/RequestListenerTest.php + path: tests/EventListener/LoginListenerTest.php - message: "#^Parameter \\#5 \\$originatedFromUri of class Symfony\\\\Component\\\\Security\\\\Core\\\\Authentication\\\\Token\\\\SwitchUserToken constructor expects string\\|null, Sentry\\\\SentryBundle\\\\Tests\\\\EventListener\\\\AuthenticatedTokenStub given\\.$#" count: 1 - path: tests/EventListener/RequestListenerTest.php + path: tests/EventListener/LoginListenerTest.php - message: "#^Call to an undefined method Symfony\\\\Component\\\\HttpKernel\\\\Event\\\\KernelEvent\\:\\:isMasterRequest\\(\\)\\.$#" diff --git a/src/DependencyInjection/Compiler/AddLoginListenerTagPass.php b/src/DependencyInjection/Compiler/AddLoginListenerTagPass.php new file mode 100644 index 00000000..24d3110c --- /dev/null +++ b/src/DependencyInjection/Compiler/AddLoginListenerTagPass.php @@ -0,0 +1,29 @@ +getDefinition(LoginListener::class); + + if (!class_exists(LoginSuccessEvent::class)) { + $listenerDefinition->addTag('kernel.event_listener', [ + 'event' => AuthenticationSuccessEvent::class, + 'method' => 'handleAuthenticationSuccessEvent', + ]); + } + } +} diff --git a/src/EventListener/LoginListener.php b/src/EventListener/LoginListener.php new file mode 100644 index 00000000..6e6d47a8 --- /dev/null +++ b/src/EventListener/LoginListener.php @@ -0,0 +1,153 @@ +hub = $hub; + $this->tokenStorage = $tokenStorage; + } + + /** + * This method is called for each request handled by the framework and + * fills the Sentry scope with information about the current user. + */ + public function handleKernelRequestEvent(RequestEvent $event): void + { + if (null === $this->tokenStorage || !$this->isMainRequest($event)) { + return; + } + + $token = $this->tokenStorage->getToken(); + + if (null !== $token) { + $this->updateUserContext($token); + } + } + + /** + * This method is called after authentication was fully successful. It allows + * to set information like the username of the currently authenticated user + * and of the impersonator, if any, on the Sentry's context. + */ + public function handleLoginSuccessEvent(LoginSuccessEvent $event): void + { + $this->updateUserContext($event->getAuthenticatedToken()); + } + + /** + * This method is called when an authentication provider authenticates the + * user. It is the event closest to {@see LoginSuccessEvent} in versions of + * the framework where it doesn't exist. + */ + public function handleAuthenticationSuccessEvent(AuthenticationSuccessEvent $event): void + { + $this->updateUserContext($event->getAuthenticationToken()); + } + + private function updateUserContext(TokenInterface $token): void + { + if (!$this->isTokenAuthenticated($token)) { + return; + } + + $client = $this->hub->getClient(); + + if (null === $client || !$client->getOptions()->shouldSendDefaultPii()) { + return; + } + + $this->hub->configureScope(function (Scope $scope) use ($token): void { + $user = $scope->getUser() ?? new UserDataBag(); + + if (null === $user->getId()) { + $user->setId($this->getUserIdentifier($token->getUser())); + } + + $impersonatorUser = $this->getImpersonatorUser($token); + + if (null !== $impersonatorUser) { + $user->setMetadata('impersonator_username', $impersonatorUser); + } + + $scope->setUser($user); + }); + } + + private function isTokenAuthenticated(TokenInterface $token): bool + { + if (method_exists($token, 'isAuthenticated') && !$token->isAuthenticated()) { + return false; + } + + return null !== $token->getUser(); + } + + /** + * @param UserInterface|\Stringable|string|null $user + */ + private function getUserIdentifier($user): ?string + { + if ($user instanceof UserInterface) { + if (method_exists($user, 'getUserIdentifier')) { + return $user->getUserIdentifier(); + } + + if (method_exists($user, 'getUsername')) { + return $user->getUsername(); + } + } + + if (\is_string($user)) { + return $user; + } + + if (\is_object($user) && method_exists($user, '__toString')) { + return (string) $user; + } + + return null; + } + + private function getImpersonatorUser(TokenInterface $token): ?string + { + if ($token instanceof SwitchUserToken) { + return $this->getUserIdentifier($token->getOriginalToken()->getUser()); + } + + return null; + } +} diff --git a/src/EventListener/RequestListener.php b/src/EventListener/RequestListener.php index b566d615..9c4951fb 100644 --- a/src/EventListener/RequestListener.php +++ b/src/EventListener/RequestListener.php @@ -9,10 +9,6 @@ use Sentry\UserDataBag; use Symfony\Component\HttpKernel\Event\ControllerEvent; use Symfony\Component\HttpKernel\Event\RequestEvent; -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\User\UserInterface; /** * This listener ensures that a new {@see \Sentry\State\Scope} is created for @@ -28,21 +24,14 @@ final class RequestListener */ private $hub; - /** - * @var TokenStorageInterface|null The token storage - */ - private $tokenStorage; - /** * Constructor. * - * @param HubInterface $hub The current hub - * @param TokenStorageInterface|null $tokenStorage The token storage + * @param HubInterface $hub The current hub */ - public function __construct(HubInterface $hub, ?TokenStorageInterface $tokenStorage) + public function __construct(HubInterface $hub/* , ?TokenStorageInterface $tokenStorage */) { $this->hub = $hub; - $this->tokenStorage = $tokenStorage; } /** @@ -63,15 +52,14 @@ public function handleKernelRequestEvent(RequestEvent $event): void return; } - $userData = new UserDataBag(); - $userData->setIpAddress($event->getRequest()->getClientIp()); + $this->hub->configureScope(static function (Scope $scope) use ($event): void { + $user = $scope->getUser() ?? new UserDataBag(); - if (null !== $this->tokenStorage) { - $this->setUserData($userData, $this->tokenStorage->getToken()); - } + if (null === $user->getIpAddress()) { + $user->setIpAddress($event->getRequest()->getClientIp()); + } - $this->hub->configureScope(static function (Scope $scope) use ($userData): void { - $scope->setUser($userData); + $scope->setUser($user); }); } @@ -97,63 +85,4 @@ public function handleKernelControllerEvent(ControllerEvent $event): void $scope->setTag('route', $route); }); } - - /** - * @param UserInterface|object|string|null $user - */ - private function getUsername($user): ?string - { - if ($user instanceof UserInterface) { - if (method_exists($user, 'getUserIdentifier')) { - return $user->getUserIdentifier(); - } - - if (method_exists($user, 'getUsername')) { - return $user->getUsername(); - } - } - - if (\is_string($user)) { - return $user; - } - - if (\is_object($user) && method_exists($user, '__toString')) { - return (string) $user; - } - - return null; - } - - private function getImpersonatorUser(TokenInterface $token): ?string - { - if (!$token instanceof SwitchUserToken) { - return null; - } - - return $this->getUsername($token->getOriginalToken()->getUser()); - } - - private function setUserData(UserDataBag $userData, ?TokenInterface $token): void - { - if (null === $token || !$this->isTokenAuthenticated($token)) { - return; - } - - $userData->setUsername($this->getUsername($token->getUser())); - - $impersonatorUser = $this->getImpersonatorUser($token); - - if (null !== $impersonatorUser) { - $userData->setMetadata('impersonator_username', $impersonatorUser); - } - } - - private function isTokenAuthenticated(TokenInterface $token): bool - { - if (method_exists($token, 'isAuthenticated') && !$token->isAuthenticated(false)) { - return false; - } - - return null !== $token->getUser(); - } } diff --git a/src/Resources/config/services.xml b/src/Resources/config/services.xml index fb6ec637..3f67a790 100644 --- a/src/Resources/config/services.xml +++ b/src/Resources/config/services.xml @@ -43,7 +43,6 @@ - @@ -87,6 +86,14 @@ + + + + + + + + diff --git a/src/SentryBundle.php b/src/SentryBundle.php index 4c909d3d..45e57eff 100644 --- a/src/SentryBundle.php +++ b/src/SentryBundle.php @@ -4,6 +4,7 @@ namespace Sentry\SentryBundle; +use Sentry\SentryBundle\DependencyInjection\Compiler\AddLoginListenerTagPass; use Sentry\SentryBundle\DependencyInjection\Compiler\CacheTracingPass; use Sentry\SentryBundle\DependencyInjection\Compiler\DbalTracingPass; use Sentry\SentryBundle\DependencyInjection\Compiler\HttpClientTracingPass; @@ -23,5 +24,6 @@ public function build(ContainerBuilder $container): void $container->addCompilerPass(new DbalTracingPass()); $container->addCompilerPass(new CacheTracingPass()); $container->addCompilerPass(new HttpClientTracingPass()); + $container->addCompilerPass(new AddLoginListenerTagPass()); } } diff --git a/tests/DependencyInjection/Compiler/AddLoginListenerTagPassTest.php b/tests/DependencyInjection/Compiler/AddLoginListenerTagPassTest.php new file mode 100644 index 00000000..cc91f8f7 --- /dev/null +++ b/tests/DependencyInjection/Compiler/AddLoginListenerTagPassTest.php @@ -0,0 +1,31 @@ +markTestSkipped('This test is incompatible with versions of Symfony where the LoginSuccessEvent event exists.'); + } + + $container = new ContainerBuilder(); + $container->register(LoginListener::class)->setPublic(true); + $container->addCompilerPass(new AddLoginListenerTagPass()); + $container->compile(); + + $listenerDefinition = $container->getDefinition(LoginListener::class); + + $this->assertSame([['event' => AuthenticationSuccessEvent::class, 'method' => 'handleAuthenticationSuccessEvent']], $listenerDefinition->getTag('kernel.event_listener')); + } +} diff --git a/tests/EventListener/LoginListenerTest.php b/tests/EventListener/LoginListenerTest.php new file mode 100644 index 00000000..44c1838b --- /dev/null +++ b/tests/EventListener/LoginListenerTest.php @@ -0,0 +1,441 @@ +hub = $this->createMock(HubInterface::class); + $this->tokenStorage = $this->createMock(TokenStorageInterface::class); + $this->listener = new LoginListener($this->hub, $this->tokenStorage); + } + + /** + * @dataProvider authenticationTokenDataProvider + * @dataProvider authenticationTokenForSymfonyVersionLowerThan54DataProvider + */ + public function testHandleKernelRequestEvent(TokenInterface $token, ?UserDataBag $user, ?UserDataBag $expectedUser): void + { + $scope = new Scope(); + + $client = $this->createMock(ClientInterface::class); + $client->expects($this->once()) + ->method('getOptions') + ->willReturn(new Options(['send_default_pii' => true])); + + $this->hub->expects($this->once()) + ->method('getClient') + ->willReturn($client); + + $this->hub->expects($this->once()) + ->method('configureScope') + ->willReturnCallback(static function (callable $callback) use ($scope): void { + $callback($scope); + }); + + $this->tokenStorage->expects($this->once()) + ->method('getToken') + ->willReturn($token); + + if (null !== $user) { + $scope->setUser($user); + } + + $this->listener->handleKernelRequestEvent(new RequestEvent( + $this->createMock(HttpKernelInterface::class), + new Request(), + HttpKernelInterface::MASTER_REQUEST + )); + + $event = $scope->applyToEvent(Event::createEvent()); + + $this->assertNotNull($event); + $this->assertEquals($expectedUser, $event->getUser()); + } + + /** + * @dataProvider authenticationTokenDataProvider + */ + public function testHandleLoginSuccessEvent(TokenInterface $token, ?UserDataBag $user, ?UserDataBag $expectedUser): void + { + if (!class_exists(LoginSuccessEvent::class)) { + $this->markTestSkipped('This test is incompatible with versions of Symfony where the LoginSuccessEvent event does not exist.'); + } + + $scope = new Scope(); + + $client = $this->createMock(ClientInterface::class); + $client->expects($this->once()) + ->method('getOptions') + ->willReturn(new Options(['send_default_pii' => true])); + + $this->hub->expects($this->once()) + ->method('getClient') + ->willReturn($client); + + $this->hub->expects($this->once()) + ->method('configureScope') + ->willReturnCallback(static function (callable $callback) use ($scope): void { + $callback($scope); + }); + + if (null !== $user) { + $scope->setUser($user); + } + + $this->listener->handleLoginSuccessEvent(new LoginSuccessEvent( + $this->createMock(AuthenticatorInterface::class), + new SelfValidatingPassport(new UserBadge('foo_passport_user')), + $token, + new Request(), + null, + 'main' + )); + + $event = $scope->applyToEvent(Event::createEvent()); + + $this->assertNotNull($event); + $this->assertEquals($expectedUser, $event->getUser()); + } + + /** + * @dataProvider authenticationTokenDataProvider + * @dataProvider authenticationTokenForSymfonyVersionLowerThan54DataProvider + */ + public function testHandleAuthenticationSuccessEvent(TokenInterface $token, ?UserDataBag $user, ?UserDataBag $expectedUser): void + { + if (class_exists(LoginSuccessEvent::class)) { + $this->markTestSkipped('This test is incompatible with versions of Symfony where the LoginSuccessEvent event exists.'); + } + + $scope = new Scope(); + + $client = $this->createMock(ClientInterface::class); + $client->expects($this->once()) + ->method('getOptions') + ->willReturn(new Options(['send_default_pii' => true])); + + $this->hub->expects($this->once()) + ->method('getClient') + ->willReturn($client); + + $this->hub->expects($this->once()) + ->method('configureScope') + ->willReturnCallback(static function (callable $callback) use ($scope): void { + $callback($scope); + }); + + if (null !== $user) { + $scope->setUser($user); + } + + $this->listener->handleAuthenticationSuccessEvent(new AuthenticationSuccessEvent($token)); + + $event = $scope->applyToEvent(Event::createEvent()); + + $this->assertNotNull($event); + $this->assertEquals($expectedUser, $event->getUser()); + } + + public function authenticationTokenDataProvider(): \Generator + { + yield 'If the username is already set on the User context, then it is not overridden' => [ + new AuthenticatedTokenStub(new UserWithIdentifierStub()), + new UserDataBag('bar_user'), + new UserDataBag('bar_user'), + ]; + + yield 'If the username is not set on the User context, then it is retrieved from the token' => [ + new AuthenticatedTokenStub(new UserWithIdentifierStub()), + null, + new UserDataBag('foo_user'), + ]; + + yield 'If the user is being impersonated, then the username of the impersonator is set on the User context' => [ + (static function (): SwitchUserToken { + if (version_compare(Kernel::VERSION, '5.0.0', '<')) { + return new SwitchUserToken( + new UserWithIdentifierStub(), + null, + 'foo_provider', + ['ROLE_USER'], + new AuthenticatedTokenStub(new UserWithIdentifierStub('bar_user')) + ); + } + + return new SwitchUserToken( + new UserWithIdentifierStub(), + 'main', + ['ROLE_USER'], + new AuthenticatedTokenStub(new UserWithIdentifierStub('bar_user')) + ); + })(), + null, + UserDataBag::createFromArray([ + 'id' => 'foo_user', + 'impersonator_username' => 'bar_user', + ]), + ]; + } + + public function authenticationTokenForSymfonyVersionLowerThan54DataProvider(): \Generator + { + if (version_compare(Kernel::VERSION, '5.4.0', '>=')) { + return; + } + + yield 'If the user is a string, then the value is used as-is' => [ + new AuthenticatedTokenStub('foo_user'), + null, + new UserDataBag('foo_user'), + ]; + + yield 'If the user is an instance of the UserInterface interface but the getUserIdentifier() method does not exist, then the getUsername() method is invoked' => [ + new AuthenticatedTokenStub(new UserWithoutIdentifierStub()), + null, + new UserDataBag('foo_user'), + ]; + + yield 'If the user is an object implementing the Stringable interface, then the __toString() method is invoked' => [ + new AuthenticatedTokenStub(new class() implements \Stringable { + public function __toString(): string + { + return 'foo_user'; + } + }), + null, + new UserDataBag('foo_user'), + ]; + } + + public function testHandleKernelRequestEventDoesNothingIfRequestIsNotMain(): void + { + $this->tokenStorage->expects($this->never()) + ->method('getToken'); + + $this->listener->handleKernelRequestEvent(new RequestEvent( + $this->createMock(HttpKernelInterface::class), + new Request(), + HttpKernelInterface::SUB_REQUEST + )); + } + + public function testHandleKernelRequestEventDoesNothingIfTokenIsNotSet(): void + { + $this->tokenStorage->expects($this->once()) + ->method('getToken') + ->willReturn(null); + + $this->listener->handleKernelRequestEvent(new RequestEvent( + $this->createMock(HttpKernelInterface::class), + new Request(), + HttpKernelInterface::MASTER_REQUEST + )); + } + + public function testHandleLoginSuccessEventDoesNothingIfTokenIsNotAuthenticated(): void + { + if (!class_exists(LoginSuccessEvent::class)) { + $this->markTestSkipped('This test is incompatible with versions of Symfony where the LoginSuccessEvent event does not exist.'); + } + + $this->hub->expects($this->never()) + ->method('getClient'); + + $this->hub->expects($this->never()) + ->method('configureScope'); + + $this->listener->handleLoginSuccessEvent(new LoginSuccessEvent( + $this->createMock(AuthenticatorInterface::class), + new SelfValidatingPassport(new UserBadge('foo_passport_user')), + new UnauthenticatedTokenStub(), + new Request(), + null, + 'main' + )); + } + + public function testHandleLoginSuccessEventDoesNothingIfClientIsNotSetOnHub(): void + { + if (!class_exists(LoginSuccessEvent::class)) { + $this->markTestSkipped('This test is incompatible with versions of Symfony where the LoginSuccessEvent event does not exist.'); + } + + $this->hub->expects($this->once()) + ->method('getClient') + ->willReturn(null); + + $this->hub->expects($this->never()) + ->method('configureScope'); + + $this->listener->handleLoginSuccessEvent(new LoginSuccessEvent( + $this->createMock(AuthenticatorInterface::class), + new SelfValidatingPassport(new UserBadge('foo_passport_user')), + new AuthenticatedTokenStub(new UserWithIdentifierStub()), + new Request(), + null, + 'main' + )); + } + + public function testHandleLoginSuccessEventDoesNothingIfSendingDefaultPiiIsDisabled(): void + { + if (!class_exists(LoginSuccessEvent::class)) { + $this->markTestSkipped('This test is incompatible with versions of Symfony where the LoginSuccessEvent event does not exist.'); + } + + $client = $this->createMock(ClientInterface::class); + $client->expects($this->once()) + ->method('getOptions') + ->willReturn(new Options(['send_default_pii' => false])); + + $this->hub->expects($this->once()) + ->method('getClient') + ->willReturn($client); + + $this->hub->expects($this->never()) + ->method('configureScope'); + + $this->listener->handleLoginSuccessEvent(new LoginSuccessEvent( + $this->createMock(AuthenticatorInterface::class), + new SelfValidatingPassport(new UserBadge('foo_passport_user')), + new AuthenticatedTokenStub(new UserWithIdentifierStub()), + new Request(), + null, + 'main' + )); + } + + public function testHandleAuthenticationSuccessEventDoesNothingIfTokenIsNotAuthenticated(): void + { + if (class_exists(LoginSuccessEvent::class)) { + $this->markTestSkipped('This test is incompatible with versions of Symfony where the LoginSuccessEvent event exists.'); + } + + $this->hub->expects($this->never()) + ->method('getClient'); + + $this->hub->expects($this->never()) + ->method('configureScope'); + + $this->listener->handleAuthenticationSuccessEvent(new AuthenticationSuccessEvent(new UnauthenticatedTokenStub())); + } + + public function testHandleAuthenticationSuccessEventDoesNothingIfClientIsNotSetOnHub(): void + { + if (class_exists(LoginSuccessEvent::class)) { + $this->markTestSkipped('This test is incompatible with versions of Symfony where the LoginSuccessEvent event exists.'); + } + + $this->hub->expects($this->once()) + ->method('getClient') + ->willReturn(null); + + $this->hub->expects($this->never()) + ->method('configureScope'); + + $this->listener->handleAuthenticationSuccessEvent(new AuthenticationSuccessEvent(new AuthenticatedTokenStub(new UserWithIdentifierStub()))); + } + + public function testHandleAuthenticationSuccessEventDoesNothingIfSendingDefaultPiiIsDisabled(): void + { + if (class_exists(LoginSuccessEvent::class)) { + $this->markTestSkipped('This test is incompatible with versions of Symfony where the LoginSuccessEvent event exists.'); + } + + $client = $this->createMock(ClientInterface::class); + $client->expects($this->once()) + ->method('getOptions') + ->willReturn(new Options(['send_default_pii' => false])); + + $this->hub->expects($this->once()) + ->method('getClient') + ->willReturn($client); + + $this->hub->expects($this->never()) + ->method('configureScope'); + + $this->listener->handleAuthenticationSuccessEvent(new AuthenticationSuccessEvent(new AuthenticatedTokenStub(new UserWithIdentifierStub()))); + } +} + +final class UnauthenticatedTokenStub extends AbstractToken +{ + public function isAuthenticated(): bool + { + return false; + } + + public function getCredentials(): ?string + { + return null; + } +} + +final class AuthenticatedTokenStub extends AbstractToken +{ + /** + * @param UserInterface|\Stringable|string|null $user + */ + public function __construct($user) + { + parent::__construct(); + + if (null !== $user) { + $this->setUser($user); + } + + if (method_exists($this, 'setAuthenticated')) { + $this->setAuthenticated(true); + } + } + + public function getCredentials(): ?string + { + return null; + } +} diff --git a/tests/EventListener/RequestListenerTest.php b/tests/EventListener/RequestListenerTest.php index 9ee2803a..440db6e7 100644 --- a/tests/EventListener/RequestListenerTest.php +++ b/tests/EventListener/RequestListenerTest.php @@ -10,8 +10,6 @@ use Sentry\Event; use Sentry\Options; use Sentry\SentryBundle\EventListener\RequestListener; -use Sentry\SentryBundle\Tests\EventListener\Fixtures\UserWithIdentifierStub; -use Sentry\SentryBundle\Tests\EventListener\Fixtures\UserWithoutIdentifierStub; use Sentry\State\HubInterface; use Sentry\State\Scope; use Sentry\UserDataBag; @@ -19,12 +17,6 @@ use Symfony\Component\HttpKernel\Event\ControllerEvent; use Symfony\Component\HttpKernel\Event\RequestEvent; use Symfony\Component\HttpKernel\HttpKernelInterface; -use Symfony\Component\HttpKernel\Kernel; -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\User\UserInterface; final class RequestListenerTest extends TestCase { @@ -33,11 +25,6 @@ final class RequestListenerTest extends TestCase */ private $hub; - /** - * @var MockObject&TokenStorageInterface - */ - private $tokenStorage; - /** * @var RequestListener */ @@ -46,28 +33,21 @@ final class RequestListenerTest extends TestCase protected function setUp(): void { $this->hub = $this->createMock(HubInterface::class); - $this->tokenStorage = $this->createMock(TokenStorageInterface::class); - $this->listener = new RequestListener($this->hub, $this->tokenStorage); + $this->listener = new RequestListener($this->hub); } /** * @dataProvider handleKernelRequestEventDataProvider - * @dataProvider handleKernelRequestEventForSymfonyVersionLowerThan54DataProvider - * @dataProvider handleKernelRequestEventForSymfonyVersionEqualTo54DataProvider - * @dataProvider handleKernelRequestEventForSymfonyVersionGreaterThan54DataProvider */ - public function testHandleKernelRequestEvent(RequestEvent $requestEvent, ?ClientInterface $client, ?TokenInterface $token, ?UserDataBag $expectedUser): void + public function testHandleKernelRequestEvent(RequestEvent $requestEvent, ClientInterface $client, UserDataBag $currentUser, UserDataBag $expectedUser): void { $scope = new Scope(); + $scope->setUser($currentUser); $this->hub->expects($this->any()) ->method('getClient') ->willReturn($client); - $this->tokenStorage->expects($this->any()) - ->method('getToken') - ->willReturn($token); - $this->hub->expects($this->any()) ->method('configureScope') ->willReturnCallback(static function (callable $callback) use ($scope): void { @@ -94,8 +74,8 @@ public function handleKernelRequestEventDataProvider(): \Generator HttpKernelInterface::SUB_REQUEST ), $this->getMockedClientWithOptions(new Options(['send_default_pii' => true])), - null, - null, + new UserDataBag(), + new UserDataBag(), ]; yield 'options.send_default_pii = FALSE' => [ @@ -105,19 +85,8 @@ public function handleKernelRequestEventDataProvider(): \Generator HttpKernelInterface::MASTER_REQUEST ), $this->getMockedClientWithOptions(new Options(['send_default_pii' => false])), - null, - null, - ]; - - yield 'token IS NULL' => [ - new RequestEvent( - $this->createMock(HttpKernelInterface::class), - new Request([], [], [], [], [], ['REMOTE_ADDR' => '127.0.0.1']), - HttpKernelInterface::MASTER_REQUEST - ), - $this->getMockedClientWithOptions(new Options(['send_default_pii' => true])), - null, - UserDataBag::createFromUserIpAddress('127.0.0.1'), + new UserDataBag(), + new UserDataBag(), ]; yield 'request.clientIp IS NULL' => [ @@ -127,214 +96,30 @@ public function handleKernelRequestEventDataProvider(): \Generator HttpKernelInterface::MASTER_REQUEST ), $this->getMockedClientWithOptions(new Options(['send_default_pii' => true])), - null, new UserDataBag(), - ]; - } - - /** - * @return \Generator - */ - public function handleKernelRequestEventForSymfonyVersionLowerThan54DataProvider(): \Generator - { - if (version_compare(Kernel::VERSION, '5.4.0', '>=')) { - return; - } - - yield 'token.authenticated = FALSE' => [ - new RequestEvent( - $this->createMock(HttpKernelInterface::class), - new Request([], [], [], [], [], ['REMOTE_ADDR' => '127.0.0.1']), - HttpKernelInterface::MASTER_REQUEST - ), - $this->getMockedClientWithOptions(new Options(['send_default_pii' => true])), - new UnauthenticatedTokenStub(), - UserDataBag::createFromUserIpAddress('127.0.0.1'), - ]; - - yield 'token.authenticated = TRUE && token.user IS NULL' => [ - new RequestEvent( - $this->createMock(HttpKernelInterface::class), - new Request([], [], [], [], [], ['REMOTE_ADDR' => '127.0.0.1']), - HttpKernelInterface::MASTER_REQUEST - ), - $this->getMockedClientWithOptions(new Options(['send_default_pii' => true])), - new AuthenticatedTokenStub(null), - UserDataBag::createFromUserIpAddress('127.0.0.1'), - ]; - - yield 'token.authenticated = TRUE && token.user INSTANCEOF string' => [ - new RequestEvent( - $this->createMock(HttpKernelInterface::class), - new Request([], [], [], [], [], ['REMOTE_ADDR' => '127.0.0.1']), - HttpKernelInterface::MASTER_REQUEST - ), - $this->getMockedClientWithOptions(new Options(['send_default_pii' => true])), - new AuthenticatedTokenStub('foo_user'), - new UserDataBag(null, null, '127.0.0.1', 'foo_user'), - ]; - - yield 'token.authenticated = TRUE && token.user INSTANCEOF UserInterface && getUserIdentifier() method DOES NOT EXISTS' => [ - new RequestEvent( - $this->createMock(HttpKernelInterface::class), - new Request([], [], [], [], [], ['REMOTE_ADDR' => '127.0.0.1']), - HttpKernelInterface::MASTER_REQUEST - ), - $this->getMockedClientWithOptions(new Options(['send_default_pii' => true])), - new AuthenticatedTokenStub(new UserWithoutIdentifierStub()), - new UserDataBag(null, null, '127.0.0.1', 'foo_user'), - ]; - - yield 'token.authenticated = TRUE && token.user INSTANCEOF object && __toString() method EXISTS' => [ - new RequestEvent( - $this->createMock(HttpKernelInterface::class), - new Request([], [], [], [], [], ['REMOTE_ADDR' => '127.0.0.1']), - HttpKernelInterface::MASTER_REQUEST - ), - $this->getMockedClientWithOptions(new Options(['send_default_pii' => true])), - new AuthenticatedTokenStub(new class() implements \Stringable { - public function __toString(): string - { - return 'foo_user'; - } - }), - new UserDataBag(null, null, '127.0.0.1', 'foo_user'), - ]; - - yield 'token.authenticated = TRUE && token INSTANCEOF SwitchUserToken' => [ - new RequestEvent( - $this->createMock(HttpKernelInterface::class), - new Request([], [], [], [], [], ['REMOTE_ADDR' => '127.0.0.1']), - HttpKernelInterface::MASTER_REQUEST - ), - $this->getMockedClientWithOptions(new Options(['send_default_pii' => true])), - new SwitchUserToken( - new UserWithIdentifierStub(), - '', - 'user_provider', - ['ROLE_USER'], - new AuthenticatedTokenStub(new UserWithIdentifierStub('foo_user_impersonator')) - ), - UserDataBag::createFromArray([ - 'ip_address' => '127.0.0.1', - 'username' => 'foo_user', - 'impersonator_username' => 'foo_user_impersonator', - ]), - ]; - } - - /** - * @return \Generator - */ - public function handleKernelRequestEventForSymfonyVersionEqualTo54DataProvider(): \Generator - { - if (version_compare(Kernel::VERSION, '5.4.0', '!=')) { - return; - } - - yield 'token.authenticated = FALSE' => [ - new RequestEvent( - $this->createMock(HttpKernelInterface::class), - new Request([], [], [], [], [], ['REMOTE_ADDR' => '127.0.0.1']), - HttpKernelInterface::MASTER_REQUEST - ), - $this->getMockedClientWithOptions(new Options(['send_default_pii' => true])), - new UnauthenticatedTokenStub(), - UserDataBag::createFromUserIpAddress('127.0.0.1'), - ]; - - yield 'token.authenticated = TRUE && token.user IS NULL' => [ - new RequestEvent( - $this->createMock(HttpKernelInterface::class), - new Request([], [], [], [], [], ['REMOTE_ADDR' => '127.0.0.1']), - HttpKernelInterface::MASTER_REQUEST - ), - $this->getMockedClientWithOptions(new Options(['send_default_pii' => true])), - new AuthenticatedTokenStub(null), - UserDataBag::createFromUserIpAddress('127.0.0.1'), - ]; - - yield 'token.authenticated = TRUE && token.user INSTANCEOF UserInterface && getUserIdentifier() method EXISTS' => [ - new RequestEvent( - $this->createMock(HttpKernelInterface::class), - new Request([], [], [], [], [], ['REMOTE_ADDR' => '127.0.0.1']), - HttpKernelInterface::MASTER_REQUEST - ), - $this->getMockedClientWithOptions(new Options(['send_default_pii' => true])), - new AuthenticatedTokenStub(new UserWithIdentifierStub()), - new UserDataBag(null, null, '127.0.0.1', 'foo_user'), - ]; - - yield 'token.authenticated = TRUE && token INSTANCEOF SwitchUserToken' => [ - new RequestEvent( - $this->createMock(HttpKernelInterface::class), - new Request([], [], [], [], [], ['REMOTE_ADDR' => '127.0.0.1']), - HttpKernelInterface::MASTER_REQUEST - ), - $this->getMockedClientWithOptions(new Options(['send_default_pii' => true])), - new SwitchUserToken( - new UserWithIdentifierStub(), - 'main', - ['ROLE_USER'], - new AuthenticatedTokenStub(new UserWithIdentifierStub('foo_user_impersonator')) - ), - UserDataBag::createFromArray([ - 'ip_address' => '127.0.0.1', - 'username' => 'foo_user', - 'impersonator_username' => 'foo_user_impersonator', - ]), - ]; - } - - /** - * @return \Generator - */ - public function handleKernelRequestEventForSymfonyVersionGreaterThan54DataProvider(): \Generator - { - if (version_compare(Kernel::VERSION, '5.4.0', '<')) { - return; - } - - yield 'token.user IS NULL' => [ - new RequestEvent( - $this->createMock(HttpKernelInterface::class), - new Request([], [], [], [], [], ['REMOTE_ADDR' => '127.0.0.1']), - HttpKernelInterface::MASTER_REQUEST - ), - $this->getMockedClientWithOptions(new Options(['send_default_pii' => true])), - new AuthenticatedTokenStub(null), - UserDataBag::createFromUserIpAddress('127.0.0.1'), + new UserDataBag(), ]; - yield 'token.user INSTANCEOF UserInterface' => [ + yield 'user.ipAddress IS NULL && request.clientIp IS NOT NULL' => [ new RequestEvent( $this->createMock(HttpKernelInterface::class), new Request([], [], [], [], [], ['REMOTE_ADDR' => '127.0.0.1']), HttpKernelInterface::MASTER_REQUEST ), $this->getMockedClientWithOptions(new Options(['send_default_pii' => true])), - new AuthenticatedTokenStub(new UserWithIdentifierStub()), - new UserDataBag(null, null, '127.0.0.1', 'foo_user'), + new UserDataBag('foo_user'), + new UserDataBag('foo_user', null, '127.0.0.1'), ]; - yield 'token INSTANCEOF SwitchUserToken' => [ + yield 'user.ipAddress IS NOT NULL && request.clientIp IS NOT NULL' => [ new RequestEvent( $this->createMock(HttpKernelInterface::class), new Request([], [], [], [], [], ['REMOTE_ADDR' => '127.0.0.1']), HttpKernelInterface::MASTER_REQUEST ), $this->getMockedClientWithOptions(new Options(['send_default_pii' => true])), - new SwitchUserToken( - new UserWithIdentifierStub(), - 'main', - ['ROLE_USER'], - new AuthenticatedTokenStub(new UserWithIdentifierStub('foo_user_impersonator')) - ), - UserDataBag::createFromArray([ - 'ip_address' => '127.0.0.1', - 'username' => 'foo_user', - 'impersonator_username' => 'foo_user_impersonator', - ]), + new UserDataBag('foo_user', null, '::1'), + new UserDataBag('foo_user', null, '::1'), ]; } @@ -412,35 +197,3 @@ private function getMockedClientWithOptions(Options $options): ClientInterface return $client; } } - -final class UnauthenticatedTokenStub extends AbstractToken -{ - public function getCredentials(): ?string - { - return null; - } -} - -final class AuthenticatedTokenStub extends AbstractToken -{ - /** - * @param UserInterface|\Stringable|string|null $user - */ - public function __construct($user) - { - parent::__construct(); - - if (method_exists($this, 'setAuthenticated')) { - $this->setAuthenticated(true); - } - - if (null !== $user) { - $this->setUser($user); - } - } - - public function getCredentials(): ?string - { - return null; - } -}