8000 feature #32841 Create impersonation_exit_path() and *_url() functions… · symfony/symfony@9d15211 · GitHub
[go: up one dir, main page]

Skip to content

Commit 9d15211

Browse files
committed
feature #32841 Create impersonation_exit_path() and *_url() functions (dFayet)
This PR was merged into the 5.2-dev branch. Discussion ---------- Create impersonation_exit_path() and *_url() functions | Q | A | ------------- | --- | Branch? | 4.4 | Bug fix? | no | New feature? | yes <!-- please update src/**/CHANGELOG.md files --> | BC breaks? | no <!-- see https://symfony.com/bc --> | Deprecations? | no <!-- please update UPGRADE-*.md and src/**/CHANGELOG.md files --> | Tests pass? | yes (not added atm) <!-- please add some, will be required by reviewers --> | Fixed tickets | #24676 <!-- #-prefixed issue number(s), if any --> | License | MIT | Doc PR | To come later <!-- symfony/symfony-docs#... --><!-- required for new features --> This is a relaunch of the PR #24737 It adds more flexibility to the previous try. You have two twig functions `impersonation_exit_url()` and `impersonation_exit_path()` You can either leave on the same path, redirect to the path where was the user at the user switch, or to the path you want. Example: The following code ```twig <p>{{ impersonation_exit_url() }}</p> <p>{{ impersonation_exit_path() }}</p> <p>&nbsp;</p> <p>{{ impersonation_exit_url(path('app_default_other')) }}</p> <p>{{ impersonation_exit_path(path('app_default_other')) }}</p> <p>&nbsp;</p> <p>{{ impersonation_exit_url('_use_impersonated_from_url') }}</p> <p>{{ impersonation_exit_path('_use_impersonated_from_url') }}</p> ``` will output ![Capture d’écran de 2019-07-31 20-58-42](https://user-images.githubusercontent.com/7721219/62239914-1482cb00-b3d6-11e9-9b58-ea8d30a2e28a.png) **Note:** If this proposal appears to be better, I'll add tests, update changelog, and prepare the doc. **Bonus:** As the "impersonated from url" is stored in the `SwitchUserToken` it might be possible to display it in the profiler: ![Capture d’écran de 2019-07-31 21-04-50](https://user-images.githubusercontent.com/7721219/62240294-efdb2300-b3d6-11e9-911a-bec48fd75327.png) WDYT? <!-- Replace this notice by a short README for your feature/bugfix. This will help people understand your PR and can be used as a start for the documentation. Additionally (see https://symfony.com/roadmap): - Bug fixes must be submitted against the lowest maintained branch where they apply (lowest branches are regularly merged to upper ones so they get the fixes too). - Features and deprecations must be submitted against branch 4.4. - Legacy code removals go to the master branch. --> Commits ------- c1e3703 Create impersonation_exit_path() and *_url() functions
2 parents 4ab612c + c1e3703 commit 9d15211

File tree

8 files changed

+138
-8
lines changed

8 files changed

+138
-8
lines changed

src/Symfony/Bridge/Twig/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ CHANGELOG
44
5.2.0
55
-----
66

7+
* added the `impersonation_exit_url()` and `impersonation_exit_path()` functions. They return a URL that allows to switch back to the original user.
78
* added the `workflow_transition()` function to easily retrieve a specific transition object
89
* added support for translating `Translatable` objects
910
* added the `t()` function to easily create `Translatable` objects

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

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use Symfony\Component\Security\Acl\Voter\FieldVote;
1515
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
1616
use Symfony\Component\Security\Core\Exception\AuthenticationCredentialsNotFoundException;
17+
use Symfony\Component\Security\Http\Impersonate\ImpersonateUrlGenerator;
1718
use Twig\Extension\AbstractExtension;
1819
use Twig\TwigFunction;
1920

@@ -26,9 +27,12 @@ final class SecurityExtension extends AbstractExtension
2627
{
2728
private $securityChecker;
2829

29-
public function __construct(AuthorizationCheckerInterface $securityChecker = null)
30+
private $impersonateUrlGenerator;
31+
32+
public function __construct(AuthorizationCheckerInterface $securityChecker = null, ImpersonateUrlGenerator $impersonateUrlGenerator = null)
3033
{
3134
$this->securityChecker = $securityChecker;
35+
$this->impersonateUrlGenerator = $impersonateUrlGenerator;
3236
}
3337

3438
/**
@@ -51,13 +55,33 @@ public function isGranted($role, $object = null, string $field = null): bool
5155
}
5256
}
5357

58+
public function getImpersonateExitUrl(string $exitTo = null): string
59+
{
60+
if (null === $this->impersonateUrlGenerator) {
61+
return '';
62+
}
63+
64+
return $this->impersonateUrlGenerator->generateExitUrl($exitTo);
65+
}
66+
67+
public function getImpersonateExitPath(string $exitTo = null): string
68+
{
69+
if (null === $this->impersonateUrlGenerator) {
70+
return '';
71+
}
72+
73+
return $this->impersonateUrlGenerator->generateExitPath($exitTo);
74+
}
75+
5476
/**
5577
* {@inheritdoc}
5678
*/
5779
public function getFunctions(): array
5880
{
5981
return [
6082
new TwigFunction('is_granted', [$this, 'isGranted']),
83+
new TwigFunction('impersonation_exit_url', [$this, 'getImpersonateExitUrl']),
84+
new TwigFunction('impersonation_exit_path', [$this, 'getImpersonateExitPath']),
6185
];
6286
}
6387
}

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
use Symfony\Component\Security\Http\Controller\UserValueResolver;
4949
use Symfony\Component\Security\Http\Firewall;
5050
use Symfony\Component\Security\Http\HttpUtils;
51+
use Symfony\Component\Security\Http\Impersonate\ImpersonateUrlGenerator;
5152
use Symfony\Component\Security\Http\Logout\LogoutUrlGenerator;
5253
use Symfony\Component\Security\Http\Session\SessionAuthenticationStrategy;
5354
use Symfony\Component\Security\Http\Session\SessionAuthenticationStrategyInterface;
@@ -160,6 +161,13 @@
160161
])
161162
->tag('security.voter', ['priority' => 245])
162163

164+
->set('security.impersonate_url_generator', ImpersonateUrlGenerator::class)
165+
->args([
166+
service('request_stack'),
167+
service('security.firewall.map'),
168+
service('security.token_storage'),
169+
])
170+
163171
// Firewall related services
164172
->set('security.firewall', FirewallListener::class)
165173
->args([

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
->set('twig.extension.security', SecurityExtension::class)
2626
->args([
2727
service('security.authorization_checker')->ignoreOnInvalid(),
28+
service('security.impersonate_url_generator')->ignoreOnInvalid(),
2829
])
2930
->tag('twig.extension')
3031
;

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

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,39 +19,47 @@
1919
class SwitchUserToken extends UsernamePasswordToken
2020
{
2121
private $originalToken;
22+
private $originatedFromUri;
2223

2324
/**
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
25+
* @param string|object $user The username (like a nickname, email address, etc.), or a UserInterface instance or an object implementing a __toString method
26+
* @param mixed $credentials This usually is the password of the user
27+
* @param string|null $originatedFromUri The URI where was the user at the switch
2628
*
2729
* @throws \InvalidArgumentException
2830
*/
29-
public function __construct($user, $credentials, string $firewallName, array $roles, TokenInterface $originalToken)
31+
public function __construct($user, $credentials, string $firewallName, array $roles, TokenInterface $originalToken, string $originatedFromUri = null)
3032
{
3133
parent::__construct($user, $credentials, $firewallName, $roles);
3234

3335
$this->originalToken = $originalToken;
36+
$this->originatedFromUri = $originatedFromUri;
3437
}
3538

3639
public function getOriginalToken(): TokenInterface
3740
{
3841
return $this->originalToken;
3942
}
4043

44+
public function getOriginatedFromUri(): ?string
45+
{
46+
return $this->originatedFromUri;
47+
}
48+
4149
/**
4250
* {@inheritdoc}
4351
*/
4452
public function __serialize(): array
4553
{
46-
return [$this->originalToken, parent::__serialize()];
54+
return [$this->originalToken, $this->originatedFromUri, parent::__serialize()];
4755
}
4856

4957
/**
5058
* {@inheritdoc}
5159
*/
5260
public function __unserialize(array $data): void
5361
{
54-
[$this->originalToken, $parentData] = $data;
62+
[$this->originalToken, $this->originatedFromUri, $parentData] = $data;
5563
$parentData = \is_array($parentData) ? $parentData : unserialize($parentData);
5664
parent::__unserialize($parentData);
5765
}

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

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ class SwitchUserTokenTest extends TestCase
2121
public function testSerialize()
2222
{
2323
$originalToken = new UsernamePasswordToken('user', 'foo', 'provider-key', ['ROLE_ADMIN', 'ROLE_ALLOWED_TO_SWITCH']);
24-
$token = new SwitchUserToken('admin', 'bar', 'provider-key', ['ROLE_USER'], $originalToken);
24+
$token = new SwitchUserToken('admin', 'bar', 'provider-key', ['ROLE_USER'], $originalToken, 'https://symfony.com/blog');
2525

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

@@ -30,6 +30,7 @@ public function testSerialize()
3030
$this->assertSame('bar', $unserializedToken->getCredentials());
3131
$this->assertSame('provider-key', $unserializedToken->getFirewallName());
3232
$this->assertEquals(['ROLE_USER'], $unserializedToken->getRoleNames());
33+
$this->assertSame('https://symfony.com/blog', $unserializedToken->getOriginatedFromUri());
3334

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

@@ -73,4 +74,14 @@ public function getSalt()
7374
$token->setUser($impersonated);
7475
$this->assertTrue($token->isAuthenticated());
7576
}
77+
78+
public function testSerializeNullImpersonateUrl()
79+
{
80+
$originalToken = new UsernamePasswordToken('user', 'foo', 'provider-key', ['ROLE_ADMIN', 'ROLE_ALLOWED_TO_SWITCH']);
81+
$token = new SwitchUserToken('admin', 'bar', 'provider-key', ['ROLE_USER'], $originalToken);
82+
83+
$unserializedToken = unserialize(serialize($token));
84+
85+
$this->assertNull($unserializedToken->getOriginatedFromUri());
86+
}
7687
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,8 @@ private function attemptSwitchUser(Request $request, string $username): ?TokenIn
181181

182182
$roles = $user->getRoles();
183183
$roles[] = 'ROLE_PREVIOUS_ADMIN';
184-
$token = new SwitchUserToken($user, $user->getPassword(), $this->firewallName, $roles, $token);
184+
$originatedFromUri = str_replace('/&', '/?', preg_replace('#[&?]'.$this->usernameParameter.'=[^&]*#', '', $request->getRequestUri()));
185+
$token = new SwitchUserToken($user, $user->getPassword(), $this->firewallName, $roles, $token, $originatedFromUri);
185186

186187
if (null !== $this->dispatcher) {
187188
$switchEvent = new SwitchUserEvent($request, $token->getUser(), $token);
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
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\Bundle\SecurityBundle\Security\FirewallMap;
15+
use Symfony\Component\HttpFoundation\RequestStack;
16+
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
17+
use Symfony\Component\Security\Core\Authentication\Token\SwitchUserToken;
18+
use Symfony\Component\Security\Http\Firewall\SwitchUserListener;
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+
private $tokenStorage;
30+
private $firewallMap;
31+
32+
public function __construct(RequestStack $requestStack, FirewallMap $firewallMap, TokenStorageInterface $tokenStorage)
33+
{
34+
$this->requestStack = $requestStack;
35+
$this->tokenStorage = $tokenStorage;
36+
$this->firewallMap = $firewallMap;
37+
}
38+
39+
public function generateExitPath(string $targetUri = null): string
40+
{
41+
return $this->buildExitPath($targetUri);
42+
}
43+
44+
public function generateExitUrl(string $targetUri = null): string
45+
{
46+
if (null === $request = $this->requestStack->getCurrentRequest()) {
47+
return '';
48+
}
49+
50+
return $request->getUriForPath($this->buildExitPath($targetUri));
51+
}
52+
53+
private function isImpersonatedUser(): bool
54+
{
55+
return $this->tokenStorage->getToken() instanceof SwitchUserToken;
56+
}
57+
58+
private function buildExitPath(string $targetUri = null): string
59+
{
60+
if (null === ($request = $this->requestStack->getCurrentRequest()) || !$this->isImpersonatedUser()) {
61+
return '';
62+
}
63+
64+
if (null === $switchUserConfig = $this->firewallMap->getFirewallConfig($request)->getSwitchUser()) {
65+
throw new \LogicException('Unable to generate the impersonate exit URL without a firewall configured for the user switch.');
66+
}
67+
68+
if (null === $targetUri) {
69+
$targetUri = $request->getRequestUri();
70+
}
71+
72+
$targetUri .= (parse_url($targetUri, \PHP_URL_QUERY) ? '&' : '?').http_build_query([$switchUserConfig['parameter'] => SwitchUserListener::EXIT_VALUE]);
73+
74+
return $targetUri;
75+
}
76+
}

0 commit comments

Comments
 (0)
0