Skip to content

Commit f898bf1

Browse files
author
dFayet
committed
Create impersonation_exit_path() and *_url() functions
1 parent 5824ab8 commit f898bf1

File tree

9 files changed

+147
-12
lines changed

9 files changed

+147
-12
lines changed

src/Symfony/Bridge/Twig/CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ CHANGELOG
44
4.4.0
55
-----
66

7+
* added the `impersonation_exit_url()` and `impersonation_exit_path()` functions that return url, that allow to switch back to the original user, when using user impersonation.
78
* deprecated to pass `$rootDir` and `$fileLinkFormatter` as 5th and 6th argument respectively to the
89
`DebugCommand::__construct()` method, swap the variables position.
910

src/Symfony/Bridge/Twig/Extension/SecurityExtension.php

+26-1
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,11 @@
1111

1212
namespace Symfony\Bridge\Twig\Extension;
1313

14+
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
1415
use Symfony\Component\Security\Acl\Voter\FieldVote;
1516
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
1617
use Symfony\Component\Security\Core\Exception\AuthenticationCredentialsNotFoundException;
18+
use Symfony\Component\Security\Http\Impersonate\ImpersonateUrlGenerator;
1719
use Twig\Extension\AbstractExtension;
1820
use Twig\TwigFunction;
1921

@@ -26,9 +28,12 @@ class SecurityExtension extends AbstractExtension
2628
{
2729
private $securityChecker;
2830

29-
public function __construct(AuthorizationCheckerInterface $securityChecker = null)
31+
private $impersonateUrlGenerator;
32+
33+
public function __construct(AuthorizationCheckerInterface $securityChecker = null, ImpersonateUrlGenerator $impersonateUrlGenerator = null)
3034
{
3135
$this->securityChecker = $securityChecker;
36+
$this->impersonateUrlGenerator = $impersonateUrlGenerator;
3237
}
3338

3439
public function isGranted($role, $object = null, $field = null)
@@ -48,13 +53,33 @@ public function isGranted($role, $object = null, $field = null)
4853
}
4954
}
5055

56+
public function getImpersonateExitUrl(string $exitTo = null)
57+
{
58+
if (null === $this->impersonateUrlGenerator) {
59+
return '';
60+
}
61+
62+
return $this->impersonateUrlGenerator->generateImpersonateExitUrl($exitTo);
63+
}
64+
65+
public function getImpersonateExitPath(string $exitTo = null)
66+
{
67+
if (null === $this->impersonateUrlGenerator) {
68+
return '';
69+
}
70+
71+
return $this->impersonateUrlGenerator->generateImpersonateExitUrl($exitTo, UrlGeneratorInterface::ABSOLUTE_PATH);
72+
}
73+
5174
/**
5275
* {@inheritdoc}
5376
*/
5477
public function getFunctions()
5578
{
5679
return [
5780
new TwigFunction('is_granted', [$this, 'isGranted']),
81+
new TwigFunction('impersonation_exit_url', [$this, 'getImpersonateExitUrl']),
82+
new TwigFunction('impersonation_exit_path', [$this, 'getImpersonateExitPath']),
5883
];
5984
}
6085

src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml

+6
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,12 @@
159159
<argument /> <!-- switch_user -->
160160
</service>
161161

162+
<service id="security.impersonate_url_generator" class="Symfony\Component\Security\Http\Impersonate\ImpersonateUrlGenerator">
163+
<argument type="service" id="request_stack" />
164+
<argument type="service" id="security.firewall.map" />
165+
<argument type="service" id="security.token_storage" />
166+
</service>
167+
162168
<service id="security.logout_url_generator" class="Symfony\Component\Security\Http\Logout\LogoutUrlGenerator">
163169
<argument type="service" id="request_stack" on-invalid="null" />
164170
<argument type="service" id="router" on-invalid="null" />

src/Symfony/Bundle/SecurityBundle/Resources/config/templating_twig.xml

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
<service id="twig.extension.security" class="Symfony\Bridge\Twig\Extension\SecurityExtension">
1616
<tag name="twig.extension" />
1717
<argument type="service" id="security.authorization_checker" on-invalid="ignore" />
18+
<argument type="service" id="security.impersonate_url_generator" on-invalid="ignore" />
1819
</service>
1920
</services>
2021
</container>

src/Symfony/Bundle/SecurityBundle/composer.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
"symfony/security-core": "^4.3",
2525
"symfony/security-csrf": "^4.2|^5.0",
2626
"symfony/security-guard": "^4.2|^5.0",
27-
"symfony/security-http": "^4.3"
27+
"symfony/security-http": "^4.4"
2828
},
2929
"require-dev": {
3030
"symfony/asset": "^3.4|^4.0|^5.0",

src/Symfony/Component/Security/Core/Authentication/Token/SwitchUserToken.php

+17-8
Original file line numberDiff line numberDiff line change
@@ -20,41 +20,50 @@ class SwitchUserToken extends UsernamePasswordToken
2020
{
2121
private $originalToken;
2222

23+
private $impersonatedFromUri;
24+
2325
/**
24-
* @param string|object $user The username (like a nickname, email address, etc.), or a UserInterface instance or an object implementing a __toString method
25-
* @param mixed $credentials This usually is the password of the user
26-
* @param string $providerKey The provider key
27-
* @param string[] $roles An array of roles
28-
* @param TokenInterface $originalToken The token of the user who switched to the current user
26+
* @param string|object $user The username (like a nickname, email address, etc.), or a UserInterface instance or an object implementing a __toString method
27+
* @param mixed $credentials This usually is the password of the user
28+
* @param string $providerKey The provider key
29+
* @param string[] $roles An array of roles
30+
* @param TokenInterface $originalToken The token of the user who switched to the current user
31+
* @param string|null $impersonatedFromUri The Url where was the user at the switch
2932
*
3033
* @throws \InvalidArgumentException
3134
*/
32-
public function __construct($user, $credentials, string $providerKey, array $roles = [], TokenInterface $originalToken)
35+
public function __construct($user, $credentials, string $providerKey, array $roles = [], TokenInterface $originalToken, string $impersonatedFromUri = null)
3336
{
3437
parent::__construct($user, $credentials, $providerKey, $roles);
3538

3639
$this->originalToken = $originalToken;
40+
$this->impersonatedFromUri = $impersonatedFromUri;
3741
}
3842

3943
public function getOriginalToken(): TokenInterface
4044
{
4145
return $this->originalToken;
4246
}
4347

48+
public function getImpersonatedFromUri(): ?string
49+
{
50+
return $this->impersonatedFromUri;
51+
}
52+
4453
/**
4554
* {@inheritdoc}
4655
*/
4756
public function __serialize(): array
4857
{
49-
return [$this->originalToken, parent::__serialize()];
58+
return [$this->originalToken, $this->impersonatedFromUri, parent::__serialize()];
5059
}
5160

5261
/**
5362
* {@inheritdoc}
5463
*/
5564
public function __unserialize(array $data): void
5665
{
57-
[$this->originalToken, $parentData] = $data;
66+
[$this->originalToken, $this->impersonatedFromUri, $parentData] = $data;
5867
parent::__unserialize($parentData);
5968
}
6069
}

src/Symfony/Component/Security/Core/Tests/Authentication/Token/SwitchUserTokenTest.php

+12-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ class SwitchUserTokenTest extends TestCase
2020
public function testSerialize()
2121
{
2222
$originalToken = new UsernamePasswordToken('user', 'foo', 'provider-key', ['ROLE_ADMIN', 'ROLE_ALLOWED_TO_SWITCH']);
23-
$token = new SwitchUserToken('admin', 'bar', 'provider-key', ['ROLE_USER'], $originalToken);
23+
$token = new SwitchUserToken('admin', 'bar', 'provider-key', ['ROLE_USER'], $originalToken, 'https://symfony.com/blog');
2424

2525
$unserializedToken = unserialize(serialize($token));
2626

@@ -29,6 +29,7 @@ public function testSerialize()
2929
$this->assertSame('bar', $unserializedToken->getCredentials());
3030
$this->assertSame('provider-key', $unserializedToken->getProviderKey());
3131
$this->assertEquals(['ROLE_USER'], $unserializedToken->getRoleNames());
32+
$this->assertSame('https://symfony.com/blog', $unserializedToken->getImpersonatedFromUri());
3233

3334
$unserializedOriginalToken = $unserializedToken->getOriginalToken();
3435

@@ -38,4 +39,14 @@ public function testSerialize()
3839
$this->assertSame('provider-key', $unserializedOriginalToken->getProviderKey());
3940
$this->assertEquals(['ROLE_ADMIN', 'ROLE_ALLOWED_TO_SWITCH'], $unserializedOriginalToken->getRoleNames());
4041
}
42+
43+
public function testSerializeNullImpersonateUrl()
44+
{
45+
$originalToken = new UsernamePasswordToken('user', 'foo', 'provider-key', ['ROLE_ADMIN', 'ROLE_ALLOWED_TO_SWITCH']);
46+
$token = new SwitchUserToken('admin', 'bar', 'provider-key', ['ROLE_USER'], $originalToken);
47+
48+
$unserializedToken = unserialize(serialize($token));
49+
50+
$this->assertNotNull($unserializedToken->getImpersonatedFromUri());
51+
}
4152
}

src/Symfony/Component/Security/Http/Firewall/SwitchUserListener.php

+8-1
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,14 @@ private function attemptSwitchUser(Request $request, $username)
153153
$roles = $user->getRoles();
154154
$roles[] = new SwitchUserRole('ROLE_PREVIOUS_ADMIN', $this->tokenStorage->getToken(), false);
155155

156-
$token = new SwitchUserToken($user, $user->getPassword(), $this->providerKey, $roles, $token);
156+
$token = new SwitchUserToken(
157+
$user,
158+
$user->getPassword(),
159+
$this->providerKey,
160+
$roles,
161+
$token,
162+
str_replace('/&', '/?', preg_replace('#[&?]'.$this->usernameParameter.'=[^&]*#', '', $request->getRequestUri()))
163+
);
157164

158165
if (null !== $this->dispatcher) {
159166
$switchEvent = new SwitchUserEvent($request, $token->getUser(), $token);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Security\Http\Impersonate;
13+
14+
use Symfony\Component\HttpFoundation\RequestStack;
15+
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
16+
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
17+
use Symfony\Component\Security\Http\Firewall\SwitchUserListener;
18+
use Symfony\Bundle\SecurityBundle\Security\FirewallMap;
19+
20+
/**
21+
* Provides generator functions for the impersonate url exit.
22+
*
23+
* @author Amrouche Hamza <hamza.simperfit@gmail.com>
24+
* @author Damien Fayet <damienf1521@gmail.com>
25+
*/
26+
class ImpersonateUrlGenerator
27+
{
28+
private $requestStack;
29+
30+
private $tokenStorage;
31+
32+
private $firewallMap;
33+
34+
public function __construct(RequestStack $requestStack, FirewallMap $firewallMap, TokenStorageInterface $tokenStorage)
35+
{
36+
$this->requestStack = $requestStack;
37+
$this->tokenStorage = $tokenStorage;
38+
$this->firewallMap = $firewallMap;
39+
}
40+
41+
public function generateImpersonateExitUrl(string $exitTo = null, $referenceType = UrlGeneratorInterface::ABSOLUTE_URL): string
42+
{
43+
$request = $this->requestStack->getCurrentRequest();
44+
45+
if (!$this->isImpersonatedUser()) {
46+
return '';
47+
}
48+
49+
if (null === $switchUserConfig = $this->firewallMap->getFirewallConfig($request)->getSwitchUser()) {
50+
throw new \LogicException('Unable to generate the impersonate exit URL without a firewall configured for the user switch.');
51+
}
52+
53+
if ('_use_impersonated_from_url' === $exitTo) {
54+
if (null === ($impersonatedFromUri = $this->tokenStorage->getToken()->getImpersonatedFromUri())) {
55+
throw new \LogicException('Unable to generate the impersonate exit URL to the origin, as the origin is unknown.');
56+
}
57+
$exitTo = $impersonatedFromUri;
58+
} elseif (null === $exitTo) {
59+
$exitTo = $request->getRequestUri();
60+
}
61+
62+
$exitTo .= (parse_url($exitTo, PHP_URL_QUERY) ? '&' : '?') . http_build_query([$switchUserConfig['parameter'] => SwitchUserListener::EXIT_VALUE]);
63+
64+
return UrlGeneratorInterface::ABSOLUTE_URL === $referenceType ? $request->getUriForPath($exitTo) : $exitTo;
65+
}
66+
67+
private function isImpersonatedUser(): bool
68+
{
69+
if (null === $token = $this->tokenStorage->getToken()) {
70+
return false;
71+
}
72+
73+
return false !== \in_array('ROLE_PREVIOUS_ADMIN', $token->getRoleNames(), true);
74+
}
75+
}

0 commit comments

Comments
 (0)