Skip to content

Commit 1cc7909

Browse files
committed
Merge branch '5.4' into 6.0
* 5.4: (33 commits) do not pass a boolean to the constructor of the Dotenv class [Notifier] Fix low-deps tests Deprecate Composer 1 [Filesystem] Add third argument `$lockFile` to `Filesystem::appendToFile()` [TwigBundle] fix auto-enabling assets/expression/routing/yaml/workflow extensions [PhpUnitBridge] fix symlink to bridge in docker by making its path relative [Dotenv] Duplicate $_SERVER values in $_ENV if they don't exist Fix markup [Notifier] Add Expo bridge Add polish translations (#43725) Rename translation:update to translation:extract [String] Add PLURAL_MAP "zombies" -- fix #43789 Added support for `statusCode` default parameter when loading a template directly from route using the `Symfony\Bundle\FrameworkBundle\Controller\TemplateController` controller. [Validator] Update validators.bs.xlf Cache voters that will always abstain [Messenger] Fix `TraceableMessageBus` implementation so it can compute caller even when used within a callback [Validator] Add the missing translations for Russian (ru) [Validator] Added missing translations for Latvian (lv) [Validator] Added missing translations [Validator] Add missing translations for Vietnamese (VI) ...
2 parents 9b17af8 + 2e8ee97 commit 1cc7909

10 files changed

+380
-7
lines changed

Authorization/AccessDecisionManager.php

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
namespace Symfony\Component\Security\Core\Authorization;
1313

1414
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
15+
use Symfony\Component\Security\Core\Authorization\Voter\CacheableVoterInterface;
1516
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
1617
use Symfony\Component\Security\Core\Exception\InvalidArgumentException;
1718

@@ -29,6 +30,8 @@ class AccessDecisionManager implements AccessDecisionManagerInterface
2930
public const STRATEGY_PRIORITY = 'priority';
3031

3132
private $voters;
33+
private $votersCacheAttributes;
34+
private $votersCacheObject;
3235
private $strategy;
3336
private $allowIfAllAbstainDecisions;
3437
private $allowIfEqualGrantedDeniedDecisions;
@@ -78,7 +81,7 @@ public function decide(TokenInterface $token, array $attributes, mixed $object =
7881
private function decideAffirmative(TokenInterface $token, array $attributes, mixed $object = null): bool
7982
{
8083
$deny = 0;
81-
foreach ($this->voters as $voter) {
84+
foreach ($this->getVoters($attributes, $object) as $voter) {
8285
$result = $voter->vote($token, $object, $attributes);
8386

8487
if (VoterInterface::ACCESS_GRANTED === $result) {
@@ -117,7 +120,7 @@ private function decideConsensus(TokenInterface $token, array $attributes, mixed
117120
{
118121
$grant = 0;
119122
$deny = 0;
120-
foreach ($this->voters as $voter) {
123+
foreach ($this->getVoters($attributes, $object) as $voter) {
121124
$result = $voter->vote($token, $object, $attributes);
122125

123126
if (VoterInterface::ACCESS_GRANTED === $result) {
@@ -153,7 +156,7 @@ private function decideConsensus(TokenInterface $token, array $attributes, mixed
153156
private function decideUnanimous(TokenInterface $token, array $attributes, mixed $object = null): bool
154157
{
155158
$grant = 0;
156-
foreach ($this->voters as $voter) {
159+
foreach ($this->getVoters($attributes, $object) as $voter) {
157160
foreach ($attributes as $attribute) {
158161
$result = $voter->vote($token, $object, [$attribute]);
159162

@@ -186,7 +189,7 @@ private function decideUnanimous(TokenInterface $token, array $attributes, mixed
186189
*/
187190
private function decidePriority(TokenInterface $token, array $attributes, mixed $object = null)
188191
{
189-
foreach ($this->voters as $voter) {
192+
foreach ($this->getVoters($attributes, $object) as $voter) {
190193
$result = $voter->vote($token, $object, $attributes);
191194

192195
if (VoterInterface::ACCESS_GRANTED === $result) {
@@ -204,4 +207,48 @@ private function decidePriority(TokenInterface $token, array $attributes, mixed
204207

205208
return $this->allowIfAllAbstainDecisions;
206209
}
210+
211+
private function getVoters(array $attributes, $object = null): iterable
212+
{
213+
$keyAttributes = [];
214+
foreach ($attributes as $attribute) {
215+
$keyAttributes[] = \is_string($attribute) ? $attribute : null;
216+
}
217+
// use `get_class` to handle anonymous classes
218+
$keyObject = \is_object($object) ? \get_class($object) : get_debug_type($object);
219+
foreach ($this->voters as $key => $voter) {
220+
if (!$voter instanceof CacheableVoterInterface) {
221+
yield $voter;
222+
continue;
223+
}
224+
225+
$supports = true;
226+
// The voter supports the attributes if it supports at least one attribute of the list
227+
foreach ($keyAttributes as $keyAttribute) {
228+
if (null === $keyAttribute) {
229+
$supports = true;
230+
} elseif (!isset($this->votersCacheAttributes[$keyAttribute][$key])) {
231+
$this->votersCacheAttributes[$keyAttribute][$key] = $supports = $voter->supportsAttribute($keyAttribute);
232+
} else {
233+
$supports = $this->votersCacheAttributes[$keyAttribute][$key];
234+
}
235+
if ($supports) {
236+
break;
237+
}
238+
}
239+
if (!$supports) {
240+
continue;
241+
}
242+
243+
if (!isset($this->votersCacheObject[$keyObject][$key])) {
244+
$this->votersCacheObject[$keyObject][$key] = $supports = $voter->supportsType($keyObject);
245+
} else {
246+
$supports = $this->votersCacheObject[$keyObject][$key];
247+
}
248+
if (!$supports) {
249+
continue;
250+
}
251+
yield $voter;
252+
}
253+
}
207254
}

Authorization/Voter/AuthenticatedVoter.php

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
* @author Fabien Potencier <fabien@symfony.com>
2525
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
2626
*/
27-
class AuthenticatedVoter implements VoterInterface
27+
class AuthenticatedVoter implements CacheableVoterInterface
2828
{
2929
public const IS_AUTHENTICATED_FULLY = 'IS_AUTHENTICATED_FULLY';
3030
public const IS_AUTHENTICATED_REMEMBERED = 'IS_AUTHENTICATED_REMEMBERED';
@@ -87,4 +87,23 @@ public function vote(TokenInterface $token, mixed $subject, array $attributes):
8787

8888
return $result;
8989
}
90+
91+
public function supportsAttribute(string $attribute): bool
92+
{
93+
return \in_array($attribute, [
94+
self::IS_AUTHENTICATED_FULLY,
95+
self::IS_AUTHENTICATED_REMEMBERED,
96+
self::IS_AUTHENTICATED_ANONYMOUSLY,
97+
self::IS_AUTHENTICATED,
98+
self::IS_ANONYMOUS,
99+
self::IS_IMPERSONATOR,
100+
self::IS_REMEMBERED,
101+
self::PUBLIC_ACCESS,
102+
], true);
103+
}
104+
105+
public function supportsType(string $subjectType): bool
106+
{
107+
return true;
108+
}
90109
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
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\Core\Authorization\Voter;
13+
14+
/**
15+
* Let voters expose the attributes and types they care about.
16+
*
17+
* By returning false to either `supportsAttribute` or `supportsType`, the
18+
* voter will never be called for the specified attribute or subject.
19+
*
20+
* @author Jérémy Derussé <jeremy@derusse.com>
21+
*/
22+
interface CacheableVoterInterface extends VoterInterface
23+
{
24+
public function supportsAttribute(string $attribute): bool;
25+
26+
/**
27+
* @param string $subjectType The type of the subject inferred by `get_class` or `get_debug_type`
28+
*/
29+
public function supportsType(string $subjectType): bool;
30+
}

Authorization/Voter/RoleVoter.php

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
*
1919
* @author Fabien Potencier <fabien@symfony.com>
2020
*/
21-
class RoleVoter implements VoterInterface
21+
class RoleVoter implements CacheableVoterInterface
2222
{
2323
private $prefix;
2424

@@ -51,6 +51,16 @@ public function vote(TokenInterface $token, mixed $subject, array $attributes):
5151
return $result;
5252
}
5353

54+
public function supportsAttribute(string $attribute): bool
55+
{
56+
return str_starts_with($attribute, $this->prefix);
57+
}
58+
59+
public function supportsType(string $subjectType): bool
60+
{
61+
return true;
62+
}
63+
5464
protected function extractRoles(TokenInterface $token)
5565
{
5666
return $token->getRoleNames();

Authorization/Voter/TraceableVoter.php

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
*
2323
* @internal
2424
*/
25-
class TraceableVoter implements VoterInterface
25+
class TraceableVoter implements CacheableVoterInterface
2626
{
2727
private $voter;
2828
private $eventDispatcher;
@@ -46,4 +46,14 @@ public function getDecoratedVoter(): VoterInterface
4646
{
4747
return $this->voter;
4848
}
49+
50+
public function supportsAttribute(string $attribute): bool
51+
{
52+
return !$this->voter instanceof CacheableVoterInterface || $this->voter->supportsAttribute($attribute);
53+
}
54+
55+
public function supportsType(string $subjectType): bool
56+
{
57+
return !$this->voter instanceof CacheableVoterInterface || $this->voter->supportsType($subjectType);
58+
}
4959
}

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ CHANGELOG
1212
5.4
1313
---
1414

15+
* Add a `CacheableVoterInterface` for voters that vote only on identified attributes and subjects
1516
* Deprecate `AuthenticationEvents::AUTHENTICATION_FAILURE`, use the `LoginFailureEvent` instead
1617
* Deprecate `AnonymousToken`, as the related authenticator was deprecated in 5.3
1718
* Deprecate `Token::getCredentials()`, tokens should no longer contain credentials (as they represent authenticated sessions)

Tests/Authorization/AccessDecisionManagerTest.php

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use PHPUnit\Framework\TestCase;
1515
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
1616
use Symfony\Component\Security\Core\Authorization\AccessDecisionManager;
17+
use Symfony\Component\Security\Core\Authorization\Voter\CacheableVoterInterface;
1718
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
1819

1920
class AccessDecisionManagerTest extends TestCase
@@ -95,6 +96,143 @@ public function getStrategyTests()
9596
];
9697
}
9798

99+
public function testCacheableVoters()
100+
{
101+
$token = $this->createMock(TokenInterface::class);
102+
$voter = $this->getMockBuilder(CacheableVoterInterface::class)->getMockForAbstractClass();
103+
$voter
104+
->expects($this->once())
105+
->method('supportsAttribute')
106+
->with('foo')
107+
->willReturn(true);
108+
$voter
109+
->expects($this->once())
110+
->method('supportsType')
111+
->with('string')
112+
->willReturn(true);
113+
$voter
114+
->expects($this->once())
115+
->method('vote')
116+
->with($token, 'bar', ['foo'])
117+
->willReturn(VoterInterface::ACCESS_GRANTED);
118+
119+
$manager = new AccessDecisionManager([$voter]);
120+
$this->assertTrue($manager->decide($token, ['foo'], 'bar'));
121+
}
122+
123+
public function testCacheableVotersIgnoresNonStringAttributes()
124+
{
125+
$token = $this->createMock(TokenInterface::class);
126+
$voter = $this->getMockBuilder(CacheableVoterInterface::class)->getMockForAbstractClass();
127+
$voter
128+
->expects($this->never())
129+
->method('supportsAttribute');
130+
$voter
131+
->expects($this->once())
132+
->method('supportsType')
133+
->with('string')
134+
->willReturn(true);
135+
$voter
136+
->expects($this->once())
137+
->method('vote')
138+
->with($token, 'bar', [1337])
139+
->willReturn(VoterInterface::ACCESS_GRANTED);
140+
141+
$manager = new AccessDecisionManager([$voter]);
142+
$this->assertTrue($manager->decide($token, [1337], 'bar'));
143+
}
144+
145+
public function testCacheableVotersWithMultipleAttributes()
146+
{
147+
$token = $this->createMock(TokenInterface::class);
148+
$voter = $this->getMockBuilder(CacheableVoterInterface::class)->getMockForAbstractClass();
149+
$voter
150+
->expects($this->exactly(2))
151+
->method('supportsAttribute')
152+
->withConsecutive(['foo'], ['bar'])
153+
->willReturnOnConsecutiveCalls(false, true);
154+
$voter
155+
->expects($this->once())
156+
->method('supportsType')
157+
->with('string')
158+
->willReturn(true);
159+
$voter
160+
->expects($this->once())
161+
->method('vote')
162+
->with($token, 'bar', ['foo', 'bar'])
163+
->willReturn(VoterInterface::ACCESS_GRANTED);
164+
165+
$manager = new AccessDecisionManager([$voter]);
166+
$this->assertTrue($manager->decide($token, ['foo', 'bar'], 'bar', true));
167+
}
168+
169+
public function testCacheableVotersWithEmptyAttributes()
170+
{
171+
$token = $this->createMock(TokenInterface::class);
172+
$voter = $this->getMockBuilder(CacheableVoterInterface::class)->getMockForAbstractClass();
173+
$voter
174+
->expects($this->never())
175+
->method('supportsAttribute');
176+
$voter
177+
->expects($this->once())
178+
->method('supportsType')
179+
->with('string')
180+
->willReturn(true);
181+
$voter
182+
->expects($this->once())
183+
->method('vote')
184+
->with($token, 'bar', [])
185+
->willReturn(VoterInterface::ACCESS_GRANTED);
186+
187+
$manager = new AccessDecisionManager([$voter]);
188+
$this->assertTrue($manager->decide($token, [], 'bar'));
189+
}
190+
191+
public function testCacheableVotersSupportsMethodsCalledOnce()
192+
{
193+
$token = $this->createMock(TokenInterface::class);
194+
$voter = $this->getMockBuilder(CacheableVoterInterface::class)->getMockForAbstractClass();
195+
$voter
196+
->expects($this->once())
197+
->method('supportsAttribute')
198+
->with('foo')
199+
->willReturn(true);
200+
$voter
201+
->expects($this->once())
202+
->method('supportsType')
203+
->with('string')
204+
->willReturn(true);
205+
$voter
206+
->expects($this->exactly(2))
207+
->method('vote')
208+
->with($token, 'bar', ['foo'])
209+
->willReturn(VoterInterface::ACCESS_GRANTED);
210+
211+
$manager = new AccessDecisionManager([$voter]);
212+
$this->assertTrue($manager->decide($token, ['foo'], 'bar'));
213+
$this->assertTrue($manager->decide($token, ['foo'], 'bar'));
214+
}
215+
216+
public function testCacheableVotersNotCalled()
217+
{
218+
$token = $this->createMock(TokenInterface::class);
219+
$voter = $this->getMockBuilder(CacheableVoterInterface::class)->getMockForAbstractClass();
220+
$voter
221+
->expects($this->once())
222+
->method('supportsAttribute')
223+
->with('foo')
224+
->willReturn(false);
225+
$voter
226+
->expects($this->never())
227+
->method('supportsType');
228+
$voter
229+
->expects($this->never())
230+
->method('vote');
231+
232+
$manager = new AccessDecisionManager([$voter]);
233+
$this->assertFalse($manager->decide($token, ['foo'], 'bar'));
234+
}
235+
98236
protected function getVoters($grants, $denies, $abstains)
99237
{
100238
$voters = [];

0 commit comments

Comments
 (0)