Skip to content

Commit

Permalink
Support logging the impersonator user, if any
Browse files Browse the repository at this point in the history
  • Loading branch information
ste93cry committed Jul 6, 2022
1 parent 6db5a35 commit eb454a8
Show file tree
Hide file tree
Showing 5 changed files with 169 additions and 62 deletions.
5 changes: 4 additions & 1 deletion CHANGELOG.md
Expand Up @@ -2,8 +2,11 @@

## Unreleased

- Support logging the impersonator user, if any (#647)

## 4.3.0 (2022-05-30)
- Fix compatibility issue with Symfony >= 6.1.0 (#635)

- Fix compatibility issue with Symfony `>= 6.1.0` (#635)
- Add `TracingDriverConnectionInterface::getNativeConnection()` method to get the original driver connection (#597)
- Add `options.http_timeout` and `options.http_connect_timeout` configuration options (#593)

Expand Down
25 changes: 15 additions & 10 deletions phpstan-baseline.neon
Expand Up @@ -155,16 +155,6 @@ parameters:
count: 1
path: src/EventListener/RequestListener.php

-
message: "#^Cannot call method getUser\\(\\) on Symfony\\\\Component\\\\Security\\\\Core\\\\Authentication\\\\Token\\\\TokenInterface\\|null\\.$#"
count: 1
path: src/EventListener/RequestListener.php

-
message: "#^Parameter \\#1 \\$user of method Sentry\\\\SentryBundle\\\\EventListener\\\\RequestListener\\:\\:getUsername\\(\\) expects object\\|string, Symfony\\\\Component\\\\Security\\\\Core\\\\User\\\\UserInterface\\|null given\\.$#"
count: 1
path: src/EventListener/RequestListener.php

-
message: "#^Call to an undefined method Symfony\\\\Component\\\\HttpKernel\\\\Event\\\\KernelEvent\\:\\:isMasterRequest\\(\\)\\.$#"
count: 1
Expand Down Expand Up @@ -285,6 +275,21 @@ parameters:
count: 1
path: tests/EventListener/RequestListenerTest.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

-
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

-
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

-
message: "#^Call to an undefined method Symfony\\\\Component\\\\HttpKernel\\\\Event\\\\KernelEvent\\:\\:isMasterRequest\\(\\)\\.$#"
count: 1
Expand Down
36 changes: 26 additions & 10 deletions src/EventListener/RequestListener.php
Expand Up @@ -10,6 +10,7 @@
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;

Expand Down Expand Up @@ -62,16 +63,11 @@ public function handleKernelRequestEvent(RequestEvent $event): void
return;
}

$token = null;
$userData = new UserDataBag();
$userData->setIpAddress($event->getRequest()->getClientIp());

if (null !== $this->tokenStorage) {
$token = $this->tokenStorage->getToken();
}

if ($this->isTokenAuthenticated($token)) {
$userData->setUsername($this->getUsername($token->getUser()));
$this->setUserData($userData, $this->tokenStorage->getToken());
}

$this->hub->configureScope(static function (Scope $scope) use ($userData): void {
Expand Down Expand Up @@ -103,7 +99,7 @@ public function handleKernelControllerEvent(ControllerEvent $event): void
}

/**
* @param UserInterface|object|string $user
* @param UserInterface|object|string|null $user
*/
private function getUsername($user): ?string
{
Expand All @@ -128,12 +124,32 @@ private function getUsername($user): ?string
return null;
}

private function isTokenAuthenticated(?TokenInterface $token): bool
private function getImpersonatorUser(TokenInterface $token): ?string
{
if (null === $token) {
return false;
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;
}
Expand Down
12 changes: 11 additions & 1 deletion tests/EventListener/Fixtures/UserWithIdentifierStub.php
Expand Up @@ -8,14 +8,24 @@

final class UserWithIdentifierStub implements UserInterface
{
/**
* @var string
*/
private $username;

public function __construct(string $username = 'foo_user')
{
$this->username = $username;
}

public function getUserIdentifier(): string
{
return $this->getUsername();
}

public function getUsername(): string
{
return 'foo_user';
return $this->username;
}

public function getRoles(): array
Expand Down
153 changes: 113 additions & 40 deletions tests/EventListener/RequestListenerTest.php
Expand Up @@ -22,6 +22,7 @@
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;

Expand Down Expand Up @@ -51,6 +52,9 @@ protected function setUp(): void

/**
* @dataProvider handleKernelRequestEventDataProvider
* @dataProvider handleKernelRequestEventForSymfonyVersionLowerThan54DataProvider
* @dataProvider handleKernelRequestEventForSymfonyVersionGreaterThan54DataProvider
* @dataProvider handleKernelRequestEventForSymfonyVersionLowerThan60DataProvider
*/
public function testHandleKernelRequestEvent(RequestEvent $requestEvent, ?ClientInterface $client, ?TokenInterface $token, ?UserDataBag $expectedUser): void
{
Expand Down Expand Up @@ -138,46 +142,6 @@ public function handleKernelRequestEventDataProvider(): \Generator
UserDataBag::createFromUserIpAddress('127.0.0.1'),
];

if (version_compare(Kernel::VERSION, '6.0.0', '<')) {
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.user INSTANCEOF UserInterface && getUserIdentifier() method EXISTS' => [
new RequestEvent(
$this->createMock(HttpKernelInterface::class),
Expand All @@ -201,6 +165,115 @@ public function __toString(): string
];
}

/**
* @return \Generator<mixed>
*/
public function handleKernelRequestEventForSymfonyVersionLowerThan54DataProvider(): \Generator
{
if (version_compare(Kernel::VERSION, '5.4.0', '>=')) {
return;
}

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<mixed>
*/
public function handleKernelRequestEventForSymfonyVersionGreaterThan54DataProvider(): \Generator
{
if (version_compare(Kernel::VERSION, '5.4.0', '<')) {
return;
}

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<mixed>
*/
public function handleKernelRequestEventForSymfonyVersionLowerThan60DataProvider(): \Generator
{
if (version_compare(Kernel::VERSION, '6.0.0', '>=')) {
return;
}

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'),
];
}

/**
* @dataProvider handleKernelControllerEventDataProvider
*
Expand Down

0 comments on commit eb454a8

Please sign in to comment.