Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit e896c7b

Browse filesBrowse files
committed
Cache voters that will always abstain
1 parent c0bf036 commit e896c7b
Copy full SHA for e896c7b

File tree

9 files changed

+360
-7
lines changed
Filter options

9 files changed

+360
-7
lines changed

‎src/Symfony/Component/Security/Core/Authorization/AccessDecisionManager.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/Security/Core/Authorization/AccessDecisionManager.php
+34-4Lines changed: 34 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;
@@ -80,7 +83,7 @@ public function decide(TokenInterface $token, array $attributes, $object = null/
8083
private function decideAffirmative(TokenInterface $token, array $attributes, $object = null): bool
8184
{
8285
$deny = 0;
83-
foreach ($this->voters as $voter) {
86+
foreach ($this->getVoters($attributes, $object) as $voter) {
8487
$result = $voter->vote($token, $object, $attributes);
8588

8689
if (VoterInterface::ACCESS_GRANTED === $result) {
@@ -119,7 +122,7 @@ private function decideConsensus(TokenInterface $token, array $attributes, $obje
119122
{
120123
$grant = 0;
121124
$deny = 0;
122-
foreach ($this->voters as $voter) {
125+
foreach ($this->getVoters($attributes, $object) as $voter) {
123126
$result = $voter->vote($token, $object, $attributes);
124127

125128
if (VoterInterface::ACCESS_GRANTED === $result) {
@@ -155,7 +158,7 @@ private function decideConsensus(TokenInterface $token, array $attributes, $obje
155158
private function decideUnanimous(TokenInterface $token, array $attributes, $object = null): bool
156159
{
157160
$grant = 0;
158-
foreach ($this->voters as $voter) {
161+
foreach ($this->getVoters($attributes, $object) as $voter) {
159162
foreach ($attributes as $attribute) {
160163
$result = $voter->vote($token, $object, [$attribute]);
161164

@@ -188,7 +191,7 @@ private function decideUnanimous(TokenInterface $token, array $attributes, $obje
188191
*/
189192
private function decidePriority(TokenInterface $token, array $attributes, $object = null)
190193
{
191-
foreach ($this->voters as $voter) {
194+
foreach ($this->getVoters($attributes, $object) as $voter) {
192195
$result = $voter->vote($token, $object, $attributes);
193196

194197
if (VoterInterface::ACCESS_GRANTED === $result) {
@@ -206,4 +209,31 @@ private function decidePriority(TokenInterface $token, array $attributes, $objec
206209

207210
return $this->allowIfAllAbstainDecisions;
208211
}
212+
213+
private function getVoters(array $attributes, $object = null): iterable
214+
{
215+
$keyAttributes = count($attributes) === 1 && is_string($attributes[0]) ? $attributes[0] : null;
216+
$keyObject = get_debug_type($object);
217+
218+
foreach ($this->voters as $key => $voter) {
219+
if (!isset($this->votersCacheAttributes[$keyAttributes][$key])) {
220+
$this->votersCacheAttributes[$keyAttributes][$key] = $supports = $keyAttributes === null || !$voter instanceof CacheableVoterInterface || $voter->supportsAttribute($keyAttributes);
221+
} else {
222+
$supports = $this->votersCacheAttributes[$keyAttributes][$key];
223+
}
224+
if (!$supports) {
225+
continue;
226+
}
227+
228+
if (!isset($this->votersCacheObject[$keyObject][$key])) {
229+
$this->votersCacheObject[$keyObject][$key] = $supports = !$voter instanceof CacheableVoterInterface || $voter->supportsType($keyObject);
230+
} else {
231+
$supports = $this->votersCacheObject[$keyObject][$key];
232+
}
233+
if (!$supports) {
234+
continue;
235+
}
236+
yield $voter;
237+
}
238+
}
209239
}

‎src/Symfony/Component/Security/Core/Authorization/Voter/AuthenticatedVoter.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/Security/Core/Authorization/Voter/AuthenticatedVoter.php
+20-1Lines 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 VoterInterface, CacheableVoterInterface
2828
{
2929
public const IS_AUTHENTICATED_FULLY = 'IS_AUTHENTICATED_FULLY';
3030
public const IS_AUTHENTICATED_REMEMBERED = 'IS_AUTHENTICATED_REMEMBERED';
@@ -116,4 +116,23 @@ public function vote(TokenInterface $token, $subject, array $attributes)
116116

117117
return $result;
118118
}
119+
120+
public function supportsAttribute(string $attribute): bool
121+
{
122+
return in_array($attribute, [
123+
self::IS_AUTHENTICATED_FULLY,
124+
self::IS_AUTHENTICATED_REMEMBERED,
125+
self::IS_AUTHENTICATED_ANONYMOUSLY,
126+
self::IS_AUTHENTICATED,
127+
self::IS_ANONYMOUS,
128+
self::IS_IMPERSONATOR,
129+
self::IS_REMEMBERED,
130+
self::PUBLIC_ACCESS,
131+
], true);
132+
}
133+
134+
public function supportsType(string $objectType): bool
135+
{
136+
return true;
137+
}
119138
}
+27Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
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+
* Marker interface for voters that know if they will always abstain for the
16+
* give attributes.
17+
*
18+
* By implementing this interface, the voter will never be called for the
19+
* specified attributes.
20+
*
21+
* @author Jérémy Derussé <jeremy@derusse.com>
22+
*/
23+
interface CacheableVoterInterface extends VoterInterface
24+
{
25+
public function supportsAttribute(string $attribute): bool;
26+
public function supportsType(string $objectType): bool;
27+
}

‎src/Symfony/Component/Security/Core/Authorization/Voter/RoleVoter.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/Security/Core/Authorization/Voter/RoleVoter.php
+11-1Lines 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 VoterInterface, CacheableVoterInterface
2222
{
2323
private $prefix;
2424

@@ -55,6 +55,16 @@ public function vote(TokenInterface $token, $subject, array $attributes)
5555
return $result;
5656
}
5757

58+
public function supportsAttribute(string $attribute): bool
59+
{
60+
return str_starts_with($attribute, $this->prefix);
61+
}
62+
63+
public function supportsType(string $objectType): bool
64+
{
65+
return true;
66+
}
67+
5868
protected function extractRoles(TokenInterface $token)
5969
{
6070
return $token->getRoleNames();

‎src/Symfony/Component/Security/Core/Authorization/Voter/TraceableVoter.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/Security/Core/Authorization/Voter/TraceableVoter.php
+11-1Lines 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 VoterInterface, 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 $objectType): bool
56+
{
57+
return !$this->voter instanceof CacheableVoterInterface || $this->voter->supportsType($objectType);
58+
}
4959
}

‎src/Symfony/Component/Security/Core/Tests/Authorization/AccessDecisionManagerTest.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/Security/Core/Tests/Authorization/AccessDecisionManagerTest.php
+136Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait;
1616
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
1717
use Symfony\Component\Security\Core\Authorization\AccessDecisionManager;
18+
use Symfony\Component\Security\Core\Authorization\Voter\CacheableVoterInterface;
1819
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
1920

2021
class AccessDecisionManagerTest extends TestCase
@@ -120,6 +121,141 @@ public function provideStrategies()
120121
yield [AccessDecisionManager::STRATEGY_PRIORITY];
121122
}
122123

124+
public function testCacheableVoters()
125+
{
126+
$token = $this->createMock(TokenInterface::class);
127+
$voter = $this->getMockBuilder(CacheableVoterInterface::class)->getMockForAbstractClass();
128+
$voter
129+
->expects($this->once())
130+
->method('supportsAttribute')
131+
->with('foo')
132+
->willReturn(true);
133+
$voter
134+
->expects($this->once())
135+
->method('supportsType')
136+
->with('string')
137+
->willReturn(true);
138+
$voter
139+
->expects($this->once())
140+
->method('vote')
141+
->with($token, 'bar', ['foo'])
142+
->willReturn(VoterInterface::ACCESS_GRANTED);
143+
144+
$manager = new AccessDecisionManager([$voter]);
145+
$this->assertSame(true, $manager->decide($token, ['foo'], 'bar'));
146+
}
147+
148+
public function testCacheableVotersIgnoresNonStringAttributes()
149+
{
150+
$token = $this->createMock(TokenInterface::class);
151+
$voter = $this->getMockBuilder(CacheableVoterInterface::class)->getMockForAbstractClass();
152+
$voter
153+
->expects($this->never())
154+
->method('supportsAttribute');
155+
$voter
156+
->expects($this->once())
157+
->method('supportsType')
158+
->with('string')
159+
->willReturn(true);
160+
$voter
161+
->expects($this->once())
162+
->method('vote')
163+
->with($token, 'bar', [1337])
164+
->willReturn(VoterInterface::ACCESS_GRANTED);
165+
166+
$manager = new AccessDecisionManager([$voter]);
167+
$this->assertSame(true, $manager->decide($token, [1337], 'bar'));
168+
}
169+
170+
public function testCacheableVotersIgnoresMultipleAttributes()
171+
{
172+
$token = $this->createMock(TokenInterface::class);
173+
$voter = $this->getMockBuilder(CacheableVoterInterface::class)->getMockForAbstractClass();
174+
$voter
175+
->expects($this->never())
176+
->method('supportsAttribute');
177+
$voter
178+
->expects($this->once())
179+
->method('supportsType')
180+
->with('string')
181+
->willReturn(true);
182+
$voter
183+
->expects($this->once())
184+
->method('vote')
185+
->with($token, 'bar', ['foo', 'bar'])
186+
->willReturn(VoterInterface::ACCESS_GRANTED);
187+
188+
$manager = new AccessDecisionManager([$voter]);
189+
$this->assertSame(true, $manager->decide($token, ['foo', 'bar'], 'bar', true));
190+
}
191+
192+
public function testCacheableVotersIgnoresEmptyAttributes()
193+
{
194+
$token = $this->createMock(TokenInterface::class);
195+
$voter = $this->getMockBuilder(CacheableVoterInterface::class)->getMockForAbstractClass();
196+
$voter
197+
->expects($this->never())
198+
->method('supportsAttribute');
199+
$voter
200+
->expects($this->once())
201+
->method('supportsType')
202+
->with('string')
203+
->willReturn(true);
204+
$voter
205+
->expects($this->once())
206+
->method('vote')
207+
->with($token, 'bar', [])
208+
->willReturn(VoterInterface::ACCESS_GRANTED);
209+
210+
$manager = new AccessDecisionManager([$voter]);
211+
$this->assertSame(true, $manager->decide($token, [], 'bar'));
212+
}
213+
214+
public function testCacheableVotersSupportsMethodsCalledOnce()
215+
{
216+
$token = $this->createMock(TokenInterface::class);
217+
$voter = $this->getMockBuilder(CacheableVoterInterface::class)->getMockForAbstractClass();
218+
$voter
219+
->expects($this->once())
220+
->method('supportsAttribute')
221+
->with('foo')
222+
->willReturn(true);
223+
$voter
224+
->expects($this->once())
225+
->method('supportsType')
226+
->with('string')
227+
->willReturn(true);
228+
$voter
229+
->expects($this->exactly(2))
230+
->method('vote')
231+
->with($token, 'bar', ['foo'])
232+
->willReturn(VoterInterface::ACCESS_GRANTED);
233+
234+
$manager = new AccessDecisionManager([$voter]);
235+
$this->assertSame(true, $manager->decide($token, ['foo'], 'bar'));
236+
$this->assertSame(true, $manager->decide($token, ['foo'], 'bar'));
237+
}
238+
239+
public function testCacheableVotersNotCalled()
240+
{
241+
$token = $this->createMock(TokenInterface::class);
242+
$voter = $this->getMockBuilder(CacheableVoterInterface::class)->getMockForAbstractClass();
243+
$voter
244+
->expects($this->once())
245+
->method('supportsAttribute')
246+
->with('foo')
247+
->willReturn(false);
248+
$voter
249+
->expects($this->never())
250+
->method('supportsType');
251+
$voter
252+
->expects($this->never())
253+
->method('vote');
254+
255+
$manager = new AccessDecisionManager([$voter]);
256+
$this->assertSame(false, $manager->decide($token, ['foo'], 'bar'));
257+
}
258+
123259
protected function getVoters($grants, $denies, $abstains)
124260
{
125261
$voters = [];

‎src/Symfony/Component/Security/Core/Tests/Authorization/Voter/AuthenticatedVoterTest.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/Security/Core/Tests/Authorization/Voter/AuthenticatedVoterTest.php
+34Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,40 @@ public function getLegacyVoteTests()
8383
];
8484
}
8585

86+
/**
87+
* @dataProvider provideAttributes
88+
*/
89+
public function testSupportsAttribute(string $attribute, bool $expected)
90+
{
91+
$voter = new AuthenticatedVoter(new AuthenticationTrustResolver());
92+
93+
$this->assertSame($expected, $voter->supportsAttribute($attribute));
94+
}
95+
96+
public function provideAttributes()
97+
{
98+
yield [AuthenticatedVoter::IS_AUTHENTICATED_FULLY, true];
99+
yield [AuthenticatedVoter::IS_AUTHENTICATED_REMEMBERED, true];
100+
yield [AuthenticatedVoter::IS_AUTHENTICATED_ANONYMOUSLY, true];
101+
yield [AuthenticatedVoter::IS_ANONYMOUS, true];
102+
yield [AuthenticatedVoter::IS_AUTHENTICATED, true];
103+
yield [AuthenticatedVoter::IS_IMPERSONATOR, true];
104+
yield [AuthenticatedVoter::IS_REMEMBERED, true];
105+
yield [AuthenticatedVoter::PUBLIC_ACCESS, true];
106+
107+
yield ['', false];
108+
yield ['foo', false];
109+
}
110+
111+
public function testSupportsType()
112+
{
113+
$voter = new AuthenticatedVoter(new AuthenticationTrustResolver());
114+
115+
$this->assertTrue($voter->supportsType(get_debug_type('foo')));
116+
$this->assertTrue($voter->supportsType(get_debug_type(null)));
117+
$this->assertTrue($voter->supportsType(get_debug_type(new \StdClass())));
118+
}
119+
86120
protected function getToken($authenticated)
87121
{
88122
$user = new InMemoryUser('wouter', '', ['ROLE_USER']);

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.