Skip to content

Commit af3cae9

Browse files
committed
Merge branch '5.4' into 6.0
* 5.4: Be more precise about the required Composer version [Security] Implement ADM strategies as dedicated classes Bump Symfony version to 5.3.11 Update VERSION for 5.3.10 Update CHANGELOG for 5.3.10
2 parents 38c150c + e60d0ab commit af3cae9

15 files changed

+652
-136
lines changed

Authorization/AccessDecisionManager.php

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

1414
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
15+
use Symfony\Component\Security\Core\Authorization\Strategy\AccessDecisionStrategyInterface;
16+
use Symfony\Component\Security\Core\Authorization\Strategy\AffirmativeStrategy;
17+
use Symfony\Component\Security\Core\Authorization\Strategy\ConsensusStrategy;
18+
use Symfony\Component\Security\Core\Authorization\Strategy\PriorityStrategy;
19+
use Symfony\Component\Security\Core\Authorization\Strategy\UnanimousStrategy;
1520
use Symfony\Component\Security\Core\Authorization\Voter\CacheableVoterInterface;
1621
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
1722
use Symfony\Component\Security\Core\Exception\InvalidArgumentException;
@@ -21,40 +26,62 @@
2126
* that use decision voters.
2227
*
2328
* @author Fabien Potencier <fabien@symfony.com>
29+
*
30+
* @final since Symfony 5.4
2431
*/
2532
class AccessDecisionManager implements AccessDecisionManagerInterface
2633
{
34+
/**
35+
* @deprecated use {@see AffirmativeStrategy} instead
36+
*/
2737
public const STRATEGY_AFFIRMATIVE = 'affirmative';
38+
39+
/**
40+
* @deprecated use {@see ConsensusStrategy} instead
41+
*/
2842
public const STRATEGY_CONSENSUS = 'consensus';
43+
44+
/**
45+
* @deprecated use {@see UnanimousStrategy} instead
46+
*/
2947
public const STRATEGY_UNANIMOUS = 'unanimous';
48+
49+
/**
50+
* @deprecated use {@see PriorityStrategy} instead
51+
*/
3052
public const STRATEGY_PRIORITY = 'priority';
3153

54+
private const VALID_VOTES = [
55+
VoterInterface::ACCESS_GRANTED => true,
56+
VoterInterface::ACCESS_DENIED => true,
57+
VoterInterface::ACCESS_ABSTAIN => true,
58+
];
59+
3260
private $voters;
3361
private $votersCacheAttributes;
3462
private $votersCacheObject;
3563
private $strategy;
36-
private $allowIfAllAbstainDecisions;
37-
private $allowIfEqualGrantedDeniedDecisions;
3864

3965
/**
40-
* @param iterable|VoterInterface[] $voters An array or an iterator of VoterInterface instances
41-
* @param string $strategy The vote strategy
42-
* @param bool $allowIfAllAbstainDecisions Whether to grant access if all voters abstained or not
43-
* @param bool $allowIfEqualGrantedDeniedDecisions Whether to grant access if result are equals
66+
* @param iterable<mixed, VoterInterface> $voters An array or an iterator of VoterInterface instances
67+
* @param AccessDecisionStrategyInterface|null $strategy The vote strategy
4468
*
4569
* @throws \InvalidArgumentException
4670
*/
47-
public function __construct(iterable $voters = [], string $strategy = self::STRATEGY_AFFIRMATIVE, bool $allowIfAllAbstainDecisions = false, bool $allowIfEqualGrantedDeniedDecisions = true)
71+
public function __construct(iterable $voters = [], /* AccessDecisionStrategyInterface */ $strategy = null)
4872
{
49-
$strategyMethod = 'decide'.ucfirst($strategy);
50-
if ('' === $strategy || !\is_callable([$this, $strategyMethod])) {
51-
throw new \InvalidArgumentException(sprintf('The strategy "%s" is not supported.', $strategy));
73+
$this->voters = $voters;
74+
if (\is_string($strategy)) {
75+
trigger_deprecation('symfony/security-core', '5.4', 'Passing the access decision strategy as a string is deprecated, pass an instance of "%s" instead.', AccessDecisionStrategyInterface::class);
76+
$allowIfAllAbstainDecisions = 3 <= \func_num_args() && func_get_arg(2);
77+
$allowIfEqualGrantedDeniedDecisions = 4 > \func_num_args() || func_get_arg(3);
78+
79+
$strategy = $this->createStrategy($strategy, $allowIfAllAbstainDecisions, $allowIfEqualGrantedDeniedDecisions);
80+
} elseif (null !== $strategy && !$strategy instanceof AccessDecisionStrategyInterface) {
81+
throw new \TypeError(sprintf('"%s": Parameter #2 ($strategy) is expected to be an instance of "%s" or null, "%s" given.', __METHOD__, AccessDecisionStrategyInterface::class, get_debug_type($strategy)));
5282
}
5383

54-
$this->voters = $voters;
55-
$this->strategy = $strategyMethod;
56-
$this->allowIfAllAbstainDecisions = $allowIfAllAbstainDecisions;
57-
$this->allowIfEqualGrantedDeniedDecisions = $allowIfEqualGrantedDeniedDecisions;
84+
$this->strategy = $strategy ?? new AffirmativeStrategy();
5885
}
5986

6087
/**
@@ -69,145 +96,50 @@ public function decide(TokenInterface $token, array $attributes, mixed $object =
6996
throw new InvalidArgumentException(sprintf('Passing more than one Security attribute to "%s()" is not supported.', __METHOD__));
7097
}
7198

72-
return $this->{$this->strategy}($token, $attributes, $object);
99+
return $this->strategy->decide(
100+
$this->collectResults($token, $attributes, $object)
101+
);
73102
}
74103

75104
/**
76-
* Grants access if any voter returns an affirmative response.
105+
* @param mixed $object
77106
*
78-
* If all voters abstained from voting, the decision will be based on the
79-
* allowIfAllAbstainDecisions property value (defaults to false).
107+
* @return \Traversable<int, int>
80108
*/
81-
private function decideAffirmative(TokenInterface $token, array $attributes, mixed $object = null): bool
109+
private function collectResults(TokenInterface $token, array $attributes, $object): \Traversable
82110
{
83-
$deny = 0;
84111
foreach ($this->getVoters($attributes, $object) as $voter) {
85112
$result = $voter->vote($token, $object, $attributes);
86-
87-
if (VoterInterface::ACCESS_GRANTED === $result) {
88-
return true;
89-
}
90-
91-
if (VoterInterface::ACCESS_DENIED === $result) {
92-
++$deny;
93-
} elseif (VoterInterface::ACCESS_ABSTAIN !== $result) {
113+
if (!\is_int($result) || !(self::VALID_VOTES[$result] ?? false)) {
94114
throw new \LogicException(sprintf('"%s::vote()" must return one of "%s" constants ("ACCESS_GRANTED", "ACCESS_DENIED" or "ACCESS_ABSTAIN"), "%s" returned.', get_debug_type($voter), VoterInterface::class, var_export($result, true)));
95115
}
96-
}
97116

98-
if ($deny > 0) {
99-
return false;
117+
yield $result;
100118
}
101-
102-
return $this->allowIfAllAbstainDecisions;
103119
}
104120

105121
/**
106-
* Grants access if there is consensus of granted against denied responses.
107-
*
108-
* Consensus means majority-rule (ignoring abstains) rather than unanimous
109-
* agreement (ignoring abstains). If you require unanimity, see
110-
* UnanimousBased.
111-
*
112-
* If there were an equal number of grant and deny votes, the decision will
113-
* be based on the allowIfEqualGrantedDeniedDecisions property value
114-
* (defaults to true).
115-
*
116-
* If all voters abstained from voting, the decision will be based on the
117-
* allowIfAllAbstainDecisions property value (defaults to false).
122+
* @throws \InvalidArgumentException if the $strategy is invalid
118123
*/
119-
private function decideConsensus(TokenInterface $token, array $attributes, mixed $object = null): bool
124+
private function createStrategy(string $strategy, bool $allowIfAllAbstainDecisions, bool $allowIfEqualGrantedDeniedDecisions): AccessDecisionStrategyInterface
120125
{
121-
$grant = 0;
122-
$deny = 0;
123-
foreach ($this->getVoters($attributes, $object) as $voter) {
124-
$result = $voter->vote($token, $object, $attributes);
125-
126-
if (VoterInterface::ACCESS_GRANTED === $result) {
127-
++$grant;
128-
} elseif (VoterInterface::ACCESS_DENIED === $result) {
129-
++$deny;
130-
} elseif (VoterInterface::ACCESS_ABSTAIN !== $result) {
131-
throw new \LogicException(sprintf('"%s::vote()" must return one of "%s" constants ("ACCESS_GRANTED", "ACCESS_DENIED" or "ACCESS_ABSTAIN"), "%s" returned.', get_debug_type($voter), VoterInterface::class, var_export($result, true)));
132-
}
133-
}
134-
135-
if ($grant > $deny) {
136-
return true;
137-
}
138-
139-
if ($deny > $grant) {
140-
return false;
126+
switch ($strategy) {
127+
case self::STRATEGY_AFFIRMATIVE:
128+
return new AffirmativeStrategy($allowIfAllAbstainDecisions);
129+
case self::STRATEGY_CONSENSUS:
130+
return new ConsensusStrategy($allowIfAllAbstainDecisions, $allowIfEqualGrantedDeniedDecisions);
131+
case self::STRATEGY_UNANIMOUS:
132+
return new UnanimousStrategy($allowIfAllAbstainDecisions);
133+
case self::STRATEGY_PRIORITY:
134+
return new PriorityStrategy($allowIfAllAbstainDecisions);
141135
}
142136

143-
if ($grant > 0) {
144-
return $this->allowIfEqualGrantedDeniedDecisions;
145-
}
146-
147-
return $this->allowIfAllAbstainDecisions;
148-
}
149-
150-
/**
151-
* Grants access if only grant (or abstain) votes were received.
152-
*
153-
* If all voters abstained from voting, the decision will be based on the
154-
* allowIfAllAbstainDecisions property value (defaults to false).
155-
*/
156-
private function decideUnanimous(TokenInterface $token, array $attributes, mixed $object = null): bool
157-
{
158-
$grant = 0;
159-
foreach ($this->getVoters($attributes, $object) as $voter) {
160-
foreach ($attributes as $attribute) {
161-
$result = $voter->vote($token, $object, [$attribute]);
162-
163-
if (VoterInterface::ACCESS_DENIED === $result) {
164-
return false;
165-
}
166-
167-
if (VoterInterface::ACCESS_GRANTED === $result) {
168-
++$grant;
169-
} elseif (VoterInterface::ACCESS_ABSTAIN !== $result) {
170-
throw new \LogicException(sprintf('"%s::vote()" must return one of "%s" constants ("ACCESS_GRANTED", "ACCESS_DENIED" or "ACCESS_ABSTAIN"), "%s" returned.', get_debug_type($voter), VoterInterface::class, var_export($result, true)));
171-
}
172-
}
173-
}
174-
175-
// no deny votes
176-
if ($grant > 0) {
177-
return true;
178-
}
179-
180-
return $this->allowIfAllAbstainDecisions;
137+
throw new \InvalidArgumentException(sprintf('The strategy "%s" is not supported.', $strategy));
181138
}
182139

183140
/**
184-
* Grant or deny access depending on the first voter that does not abstain.
185-
* The priority of voters can be used to overrule a decision.
186-
*
187-
* If all voters abstained from voting, the decision will be based on the
188-
* allowIfAllAbstainDecisions property value (defaults to false).
141+
* @return iterable<mixed, VoterInterface>
189142
*/
190-
private function decidePriority(TokenInterface $token, array $attributes, mixed $object = null)
191-
{
192-
foreach ($this->getVoters($attributes, $object) as $voter) {
193-
$result = $voter->vote($token, $object, $attributes);
194-
195-
if (VoterInterface::ACCESS_GRANTED === $result) {
196-
return true;
197-
}
198-
199-
if (VoterInterface::ACCESS_DENIED === $result) {
200-
return false;
201-
}
202-
203-
if (VoterInterface::ACCESS_ABSTAIN !== $result) {
204-
throw new \LogicException(sprintf('"%s::vote()" must return one of "%s" constants ("ACCESS_GRANTED", "ACCESS_DENIED" or "ACCESS_ABSTAIN"), "%s" returned.', get_debug_type($voter), VoterInterface::class, var_export($result, true)));
205-
}
206-
}
207-
208-
return $this->allowIfAllAbstainDecisions;
209-
}
210-
211143
private function getVoters(array $attributes, $object = null): iterable
212144
{
213145
$keyAttributes = [];
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
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\Strategy;
13+
14+
/**
15+
* A strategy for turning a stream of votes into a final decision.
16+
*
17+
* @author Alexander M. Turek <me@derrabus.de>
18+
*/
19+
interface AccessDecisionStrategyInterface
20+
{
21+
/**
22+
* @param \Traversable<int> $results
23+
*/
24+
public function decide(\Traversable $results): bool;
25+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
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\Strategy;
13+
14+
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
15+
16+
/**
17+
* Grants access if any voter returns an affirmative response.
18+
*
19+
* If all voters abstained from voting, the decision will be based on the
20+
* allowIfAllAbstainDecisions property value (defaults to false).
21+
*
22+
* @author Fabien Potencier <fabien@symfony.com>
23+
* @author Alexander M. Turek <me@derrabus.de>
24+
*/
25+
final class AffirmativeStrategy implements AccessDecisionStrategyInterface, \Stringable
26+
{
27+
/**
28+
* @var bool
29+
*/
30+
private $allowIfAllAbstainDecisions;
31+
32+
public function __construct(bool $allowIfAllAbstainDecisions = false)
33+
{
34+
$this->allowIfAllAbstainDecisions = $allowIfAllAbstainDecisions;
35+
}
36+
37+
/**
38+
* {@inheritdoc}
39+
*/
40+
public function decide(\Traversable $results): bool
41+
{
42+
$deny = 0;
43+
foreach ($results as $result) {
44+
if (VoterInterface::ACCESS_GRANTED === $result) {
45+
return true;
46+
}
47+
48+
if (VoterInterface::ACCESS_DENIED === $result) {
49+
++$deny;
50+
}
51+
}
52+
53+
if ($deny > 0) {
54+
return false;
55+
}
56+
57+
return $this->allowIfAllAbstainDecisions;
58+
}
59+
60+
public function __toString(): string
61+
{
62+
return 'affirmative';
63+
}
64+
}

0 commit comments

Comments
 (0)