Skip to content

Commit

Permalink
Improve logging of the user when he logged in programmatically (#720)
Browse files Browse the repository at this point in the history
Co-authored-by: Michi Hoffmann <[email protected]>
  • Loading branch information
ste93cry and cleptric authored Jul 31, 2023
1 parent 5472681 commit d1e5128
Show file tree
Hide file tree
Showing 10 changed files with 703 additions and 347 deletions.
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
20 changes: 15 additions & 5 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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\\>, 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\\<int, string\\> 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\\(\\)\\.$#"
Expand Down
29 changes: 29 additions & 0 deletions src/DependencyInjection/Compiler/AddLoginListenerTagPass.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

declare(strict_types=1);

namespace Sentry\SentryBundle\DependencyInjection\Compiler;

use Sentry\SentryBundle\EventListener\LoginListener;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\Security\Core\Event\AuthenticationSuccessEvent;
use Symfony\Component\Security\Http\Event\LoginSuccessEvent;

final class AddLoginListenerTagPass implements CompilerPassInterface
{
/**
* {@inheritdoc}
*/
public function process(ContainerBuilder $container): void
{
$listenerDefinition = $container->getDefinition(LoginListener::class);

if (!class_exists(LoginSuccessEvent::class)) {
$listenerDefinition->addTag('kernel.event_listener', [
'event' => AuthenticationSuccessEvent::class,
'method' => 'handleAuthenticationSuccessEvent',
]);
}
}
}
153 changes: 153 additions & 0 deletions src/EventListener/LoginListener.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
<?php

declare(strict_types=1);

namespace Sentry\SentryBundle\EventListener;

use Sentry\State\HubInterface;
use Sentry\State\Scope;
use Sentry\UserDataBag;
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\Event\AuthenticationSuccessEvent;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Http\Event\LoginSuccessEvent;

final class LoginListener
{
use KernelEventForwardCompatibilityTrait;

/**
* @var HubInterface The current hub
*/
private $hub;

/**
* @var TokenStorageInterface|null The token storage
*/
private $tokenStorage;

/**
* Constructor.
*
* @param HubInterface $hub The current hub
* @param TokenStorageInterface|null $tokenStorage The token storage
*/
public function __construct(HubInterface $hub, ?TokenStorageInterface $tokenStorage)
{
$this->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;
}
}
87 changes: 8 additions & 79 deletions src/EventListener/RequestListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;
}

/**
Expand All @@ -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);
});
}

Expand All @@ -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();
}
}
9 changes: 8 additions & 1 deletion src/Resources/config/services.xml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@

<service id="Sentry\SentryBundle\EventListener\RequestListener" class="Sentry\SentryBundle\EventListener\RequestListener">
<argument type="service" id="Sentry\State\HubInterface" />
<argument type="service" id="security.token_storage" on-invalid="ignore" />

<tag name="kernel.event_listener" event="kernel.request" method="handleKernelRequestEvent" priority="5" />
<tag name="kernel.event_listener" event="kernel.controller" method="handleKernelControllerEvent" priority="10" />
Expand Down Expand Up @@ -87,6 +86,14 @@
<tag name="kernel.event_listener" event="Symfony\Component\Messenger\Event\WorkerMessageHandledEvent" method="handleWorkerMessageHandledEvent" priority="50" />
</service>

<service id="Sentry\SentryBundle\EventListener\LoginListener" class="Sentry\SentryBundle\EventListener\LoginListener">
<argument type="service" id="Sentry\State\HubInterface" />
<argument type="service" id="security.token_storage" on-invalid="ignore" />

<tag name="kernel.event_listener" event="kernel.request" method="handleKernelRequestEvent" />
<tag name="kernel.event_listener" event="Symfony\Component\Security\Http\Event\LoginSuccessEvent" method="handleLoginSuccessEvent" />
</service>

<service id="Sentry\SentryBundle\Command\SentryTestCommand" class="Sentry\SentryBundle\Command\SentryTestCommand">
<tag name="console.command" command="sentry:test" />
</service>
Expand Down
Loading

0 comments on commit d1e5128

Please sign in to comment.