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 2d557cc

Browse filesBrowse files
committed
Cache voters that will always abstain
1 parent ec34dd5 commit 2d557cc
Copy full SHA for 2d557cc

File tree

Expand file treeCollapse file tree

10 files changed

+363
-7
lines changed
Filter options
Expand file treeCollapse file tree

10 files changed

+363
-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 = 1 === \count($attributes) && \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 = null === $keyAttributes || !$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 $subjectType): bool
135+
{
136+
return true;
137+
}
119138
}
+32Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
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 supports or will always
16+
* abstain for the give attribute and subjects
17+
*
18+
* By implementing this interface and returning false to either
19+
* `supportsAttribute` or `supportsType`, the voter will never be called for
20+
* the specified attribute or subject.
21+
*
22+
* @author Jérémy Derussé <jeremy@derusse.com>
23+
*/
24+
interface CacheableVoterInterface extends VoterInterface
25+
{
26+
public function supportsAttribute(string $attribute): bool;
27+
28+
/**
29+
* @param string $subjectType the type of the subject inferred by `get_debug_type`
30+
*/
31+
public function supportsType(string $subjectType): bool;
32+
}

‎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 $subjectType): 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 $subjectType): bool
56+
{
57+
return !$this->voter instanceof CacheableVoterInterface || $this->voter->supportsType($subjectType);
58+
}
4959
}

‎src/Symfony/Component/Security/Core/CHANGELOG.md

Copy file name to clipboardExpand all lines: src/Symfony/Component/Security/Core/CHANGELOG.md
+1Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ CHANGELOG
44
5.4
55
---
66

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

‎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->assertTrue($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->assertTrue($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->assertTrue($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->assertTrue($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->assertTrue($manager->decide($token, ['foo'], 'bar'));
236+
$this->assertTrue($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->assertFalse($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.