Skip to content

Commit 97778ec

Browse files
[Security] Improve DX of recent additions
1 parent 4f59544 commit 97778ec

File tree

10 files changed

+76
-188
lines changed

10 files changed

+76
-188
lines changed

Authentication/Token/UserAuthorizationCheckerToken.php

Lines changed: 0 additions & 31 deletions
This file was deleted.

Authorization/AuthorizationChecker.php

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,11 @@
1111

1212
namespace Symfony\Component\Security\Core\Authorization;
1313

14+
use Symfony\Component\Security\Core\Authentication\Token\AbstractToken;
1415
use Symfony\Component\Security\Core\Authentication\Token\NullToken;
16+
use Symfony\Component\Security\Core\Authentication\Token\OfflineTokenInterface;
1517
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
18+
use Symfony\Component\Security\Core\User\UserInterface;
1619

1720
/**
1821
* AuthorizationChecker is the main authorization point of the Security component.
@@ -22,8 +25,9 @@
2225
* @author Fabien Potencier <[email protected]>
2326
* @author Johannes M. Schmitt <[email protected]>
2427
*/
25-
class AuthorizationChecker implements AuthorizationCheckerInterface
28+
class AuthorizationChecker implements AuthorizationCheckerInterface, UserAuthorizationCheckerInterface
2629
{
30+
private array $tokenStack = [];
2731
private array $accessDecisionStack = [];
2832

2933
public function __construct(
@@ -34,7 +38,7 @@ public function __construct(
3438

3539
final public function isGranted(mixed $attribute, mixed $subject = null, ?AccessDecision $accessDecision = null): bool
3640
{
37-
$token = $this->tokenStorage->getToken();
41+
$token = end($this->tokenStack) ?: $this->tokenStorage->getToken();
3842

3943
if (!$token || !$token->getUser()) {
4044
$token = new NullToken();
@@ -48,4 +52,17 @@ final public function isGranted(mixed $attribute, mixed $subject = null, ?Access
4852
array_pop($this->accessDecisionStack);
4953
}
5054
}
55+
56+
final public function isGrantedForUser(UserInterface $user, mixed $attribute, mixed $subject = null, ?AccessDecision $accessDecision = null): bool
57+
{
58+
$token = new class($user->getRoles()) extends AbstractToken implements OfflineTokenInterface {};
59+
$token->setUser($user);
60+
$this->tokenStack[] = $token;
61+
62+
try {
63+
return $this->isGranted($attribute, $subject, $accessDecision);
64+
} finally {
65+
array_pop($this->tokenStack);
66+
}
67+
}
5168
}

Authorization/UserAuthorizationChecker.php

Lines changed: 0 additions & 31 deletions
This file was deleted.

Authorization/Voter/ClosureVoter.php

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,30 +11,22 @@
1111

1212
namespace Symfony\Component\Security\Core\Authorization\Voter;
1313

14-
use Symfony\Component\Security\Core\Authentication\AuthenticationTrustResolverInterface;
1514
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
16-
use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface;
15+
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
1716
use Symfony\Component\Security\Http\Attribute\IsGranted;
17+
use Symfony\Component\Security\Http\Attribute\IsGrantedContext;
1818

1919
/**
2020
* This voter allows using a closure as the attribute being voted on.
2121
*
22-
* The following named arguments are passed to the closure:
23-
*
24-
* - `token`: The token being used for voting
25-
* - `subject`: The subject of the vote
26-
* - `accessDecisionManager`: The access decision manager
27-
* - `trustResolver`: The trust resolver
28-
*
2922
* @see IsGranted doc for the complete closure signature.
3023
*
3124
* @author Alexandre Daubois <[email protected]>
3225
*/
3326
final class ClosureVoter implements CacheableVoterInterface
3427
{
3528
public function __construct(
36-
private AccessDecisionManagerInterface $accessDecisionManager,
37-
private AuthenticationTrustResolverInterface $trustResolver,
29+
private AuthorizationCheckerInterface $authorizationChecker,
3830
) {
3931
}
4032

@@ -51,6 +43,7 @@ public function supportsType(string $subjectType): bool
5143
public function vote(TokenInterface $token, mixed $subject, array $attributes, ?Vote $vote = null): int
5244
{
5345
$vote ??= new Vote();
46+
$context = new IsGrantedContext($token, $token->getUser(), $this->authorizationChecker);
5447
$failingClosures = [];
5548
$result = VoterInterface::ACCESS_ABSTAIN;
5649
foreach ($attributes as $attribute) {
@@ -60,7 +53,7 @@ public function vote(TokenInterface $token, mixed $subject, array $attributes, ?
6053

6154
$name = (new \ReflectionFunction($attribute))->name;
6255
$result = VoterInterface::ACCESS_DENIED;
63-
if ($attribute(token: $token, subject: $subject, accessDecisionManager: $this->accessDecisionManager, trustResolver: $this->trustResolver)) {
56+
if ($attribute($context, $subject)) {
6457
$vote->reasons[] = \sprintf('Closure %s returned true.', $name);
6558

6659
return VoterInterface::ACCESS_GRANTED;

CHANGELOG.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,7 @@ CHANGELOG
44
7.3
55
---
66

7-
* Add `UserAuthorizationChecker::isGrantedForUser()` to test user authorization without relying on the session.
8-
For example, users not currently logged in, or while processing a message from a message queue.
7+
* Add `UserAuthorizationCheckerInterface` to test user authorization without relying on the session
98
* Add `OfflineTokenInterface` to mark tokens that do not represent the currently logged-in user
109
* Deprecate `UserInterface::eraseCredentials()` and `TokenInterface::eraseCredentials()`,
1110
erase credentials e.g. using `__serialize()` instead

Tests/Authentication/Token/UserAuthorizationCheckerTokenTest.php

Lines changed: 0 additions & 26 deletions
This file was deleted.

Tests/Authorization/AuthorizationCheckerTest.php

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use PHPUnit\Framework\MockObject\MockObject;
1515
use PHPUnit\Framework\TestCase;
1616
use Symfony\Component\Security\Core\Authentication\Token\NullToken;
17+
use Symfony\Component\Security\Core\Authentication\Token\OfflineTokenInterface;
1718
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;
1819
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
1920
use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface;
@@ -77,4 +78,42 @@ public function testIsGrantedWithObjectAttribute()
7778
$this->tokenStorage->setToken($token);
7879
$this->assertTrue($this->authorizationChecker->isGranted($attribute));
7980
}
81+
82+
/**
83+
* @dataProvider isGrantedForUserProvider
84+
*/
85+
public function testIsGrantedForUser(bool $decide, array $roles)
86+
{
87+
$user = new InMemoryUser('username', 'password', $roles);
88+
89+
$this->accessDecisionManager
90+
->expects($this->once())
91+
->method('decide')
92+
->with($this->callback(static fn (OfflineTokenInterface $token) => $token->getUser() === $user), ['ROLE_FOO'])
93+
->willReturn($decide);
94+
95+
$this->assertSame($decide, $this->authorizationChecker->isGrantedForUser($user, 'ROLE_FOO'));
96+
}
97+
98+
public static function isGrantedForUserProvider(): array
99+
{
100+
return [
101+
[false, ['ROLE_USER']],
102+
[true, ['ROLE_USER', 'ROLE_FOO']],
103+
];
104+
}
105+
106+
public function testIsGrantedForUserWithObjectAttribute()
107+
{
108+
$attribute = new \stdClass();
109+
110+
$user = new InMemoryUser('username', 'password', ['ROLE_USER']);
111+
112+
$this->accessDecisionManager
113+
->expects($this->once())
114+
->method('decide')
115+
->with($this->isInstanceOf(OfflineTokenInterface::class), [$attribute])
116+
->willReturn(true);
117+
$this->assertTrue($this->authorizationChecker->isGrantedForUser($user, $attribute));
118+
}
80119
}

Tests/Authorization/UserAuthorizationCheckerTest.php

Lines changed: 0 additions & 70 deletions
This file was deleted.

Tests/Authorization/Voter/AuthenticatedVoterTest.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@
1515
use Symfony\Component\Security\Core\Authentication\AuthenticationTrustResolver;
1616
use Symfony\Component\Security\Core\Authentication\Token\AbstractToken;
1717
use Symfony\Component\Security\Core\Authentication\Token\NullToken;
18+
use Symfony\Component\Security\Core\Authentication\Token\OfflineTokenInterface;
1819
use Symfony\Component\Security\Core\Authentication\Token\RememberMeToken;
1920
use Symfony\Component\Security\Core\Authentication\Token\SwitchUserToken;
20-
use Symfony\Component\Security\Core\Authentication\Token\UserAuthorizationCheckerToken;
2121
use Symfony\Component\Security\Core\Authorization\Voter\AuthenticatedVoter;
2222
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
2323
use Symfony\Component\Security\Core\Exception\InvalidArgumentException;
@@ -148,7 +148,7 @@ public function getCredentials()
148148
}
149149

150150
if ('offline' === $authenticated) {
151-
return new UserAuthorizationCheckerToken($user);
151+
return new class($user->getRoles()) extends AbstractToken implements OfflineTokenInterface {};
152152
}
153153

154154
return new NullToken();

0 commit comments

Comments
 (0)