diff --git a/UPGRADE-7.1.md b/UPGRADE-7.1.md
index 9313b5e02f197..12938637ce814 100644
--- a/UPGRADE-7.1.md
+++ b/UPGRADE-7.1.md
@@ -22,6 +22,20 @@ FrameworkBundle
* Mark classes `ConfigBuilderCacheWarmer`, `Router`, `SerializerCacheWarmer`, `TranslationsCacheWarmer`, `Translator` and `ValidatorCacheWarmer` as `final`
+Security
+--------
+
+ * Add method `getDecision()` to `AccessDecisionStrategyInterface`
+ * Deprecate `AccessDecisionStrategyInterface::decide()` in favor of `AccessDecisionStrategyInterface::getDecision()`
+ * Add method `getVote()` to `VoterInterface`
+ * Deprecate `VoterInterface::vote()` in favor of `AccessDecisionStrategyInterface::getVote()`
+ * Deprecate returning `bool` from `Voter::voteOnAttribute()` (it must return a `Vote`)
+ * Add method `getDecision()` to `AccessDecisionManagerInterface`
+ * Deprecate `AccessDecisionManagerInterface::decide()` in favor of `AccessDecisionManagerInterface::getDecision()`
+ * Add method `getDecision()` to `AuthorizationCheckerInterface`
+ * Add methods `setAccessDecision()` and `getAccessDecision()` to `AccessDeniedException`
+ * Add method `getDecision()` to `Security`
+
SecurityBundle
--------------
diff --git a/src/Symfony/Bundle/FrameworkBundle/Controller/AbstractController.php b/src/Symfony/Bundle/FrameworkBundle/Controller/AbstractController.php
index f0c1d98ee01be..ca25a1245243f 100644
--- a/src/Symfony/Bundle/FrameworkBundle/Controller/AbstractController.php
+++ b/src/Symfony/Bundle/FrameworkBundle/Controller/AbstractController.php
@@ -35,6 +35,7 @@
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
+use Symfony\Component\Security\Core\Authorization\AccessDecision;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use Symfony\Component\Security\Core\User\UserInterface;
@@ -210,10 +211,22 @@ protected function isGranted(mixed $attribute, mixed $subject = null): bool
*/
protected function denyAccessUnlessGranted(mixed $attribute, mixed $subject = null, string $message = 'Access Denied.'): void
{
- if (!$this->isGranted($attribute, $subject)) {
+ if (!$this->container->has('security.authorization_checker')) {
+ throw new \LogicException('The SecurityBundle is not registered in your application. Try running "composer require symfony/security-bundle".');
+ }
+
+ $checker = $this->container->get('security.authorization_checker');
+ if (method_exists($checker, 'getDecision')) {
+ $decision = $checker->getDecision($attribute, $subject);
+ } else {
+ $decision = $checker->isGranted($attribute, $subject) ? AccessDecision::createGranted() : AccessDecision::createDenied();
+ }
+
+ if (!$decision->isGranted()) {
$exception = $this->createAccessDeniedException($message);
$exception->setAttributes([$attribute]);
$exception->setSubject($subject);
+ $exception->setAccessDecision($decision);
throw $exception;
}
diff --git a/src/Symfony/Bundle/FrameworkBundle/composer.json b/src/Symfony/Bundle/FrameworkBundle/composer.json
index af934f35df91f..ab522fa709b08 100644
--- a/src/Symfony/Bundle/FrameworkBundle/composer.json
+++ b/src/Symfony/Bundle/FrameworkBundle/composer.json
@@ -95,7 +95,7 @@
"symfony/scheduler": "<6.4.4|>=7.0.0,<7.0.4",
"symfony/serializer": "<6.4",
"symfony/security-csrf": "<6.4",
- "symfony/security-core": "<6.4",
+ "symfony/security-core": "<7.1",
"symfony/stopwatch": "<6.4",
"symfony/translation": "<6.4",
"symfony/twig-bridge": "<6.4",
diff --git a/src/Symfony/Bundle/SecurityBundle/EventListener/VoteListener.php b/src/Symfony/Bundle/SecurityBundle/EventListener/VoteListener.php
index 34ca91c3a7735..34ef8ae2088e1 100644
--- a/src/Symfony/Bundle/SecurityBundle/EventListener/VoteListener.php
+++ b/src/Symfony/Bundle/SecurityBundle/EventListener/VoteListener.php
@@ -33,7 +33,7 @@ public function __construct(TraceableAccessDecisionManager $traceableAccessDecis
public function onVoterVote(VoteEvent $event): void
{
- $this->traceableAccessDecisionManager->addVoterVote($event->getVoter(), $event->getAttributes(), $event->getVote());
+ $this->traceableAccessDecisionManager->addVoterVote($event->getVoter(), $event->getAttributes(), $event->getVoteDecision());
}
public static function getSubscribedEvents(): array
diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/views/Collector/security.html.twig b/src/Symfony/Bundle/SecurityBundle/Resources/views/Collector/security.html.twig
index 4dd0b021fe9d2..758ddefe91f11 100644
--- a/src/Symfony/Bundle/SecurityBundle/Resources/views/Collector/security.html.twig
+++ b/src/Symfony/Bundle/SecurityBundle/Resources/views/Collector/security.html.twig
@@ -476,16 +476,17 @@
attribute {{ voter_detail['attributes'][0] }} |
{% endif %}
- {% if voter_detail['vote'] == constant('Symfony\\Component\\Security\\Core\\Authorization\\Voter\\VoterInterface::ACCESS_GRANTED') %}
+ {% if voter_detail['vote'].access == constant('Symfony\\Component\\Security\\Core\\Authorization\\Voter\\VoterInterface::ACCESS_GRANTED') %}
ACCESS GRANTED
- {% elseif voter_detail['vote'] == constant('Symfony\\Component\\Security\\Core\\Authorization\\Voter\\VoterInterface::ACCESS_ABSTAIN') %}
+ {% elseif voter_detail['vote'].access == constant('Symfony\\Component\\Security\\Core\\Authorization\\Voter\\VoterInterface::ACCESS_ABSTAIN') %}
ACCESS ABSTAIN
- {% elseif voter_detail['vote'] == constant('Symfony\\Component\\Security\\Core\\Authorization\\Voter\\VoterInterface::ACCESS_DENIED') %}
+ {% elseif voter_detail['vote'].access == constant('Symfony\\Component\\Security\\Core\\Authorization\\Voter\\VoterInterface::ACCESS_DENIED') %}
ACCESS DENIED
{% else %}
- unknown ({{ voter_detail['vote'] }})
+ unknown ({{ voter_detail['vote'].access }})
{% endif %}
|
+ {{ voter_detail['vote'].message|default('~') }} |
{% endfor %}
diff --git a/src/Symfony/Bundle/SecurityBundle/Security.php b/src/Symfony/Bundle/SecurityBundle/Security.php
index c4b505d8981c7..0a7de159e4aa6 100644
--- a/src/Symfony/Bundle/SecurityBundle/Security.php
+++ b/src/Symfony/Bundle/SecurityBundle/Security.php
@@ -17,6 +17,7 @@
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
+use Symfony\Component\Security\Core\Authorization\AccessDecision;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
use Symfony\Component\Security\Core\Exception\LogicException;
use Symfony\Component\Security\Core\Exception\LogoutException;
@@ -59,8 +60,20 @@ public function getUser(): ?UserInterface
*/
public function isGranted(mixed $attributes, mixed $subject = null): bool
{
- return $this->container->get('security.authorization_checker')
- ->isGranted($attributes, $subject);
+ return $this->getDecision($attributes, $subject)->isGranted();
+ }
+
+ /**
+ * Get the access decision against the current authentication token and optionally supplied subject.
+ */
+ public function getDecision(mixed $attribute, mixed $subject = null): AccessDecision
+ {
+ $checker = $this->container->get('security.authorization_checker');
+ if (method_exists($checker, 'getDecision')) {
+ return $checker->getDecision($attribute, $subject);
+ }
+
+ return $checker->isGranted($attribute, $subject) ? AccessDecision::createGranted() : AccessDecision::createDenied();
}
public function getToken(): ?TokenInterface
diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DataCollector/SecurityDataCollectorTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/DataCollector/SecurityDataCollectorTest.php
index e4173b1fdc659..5c7accd837493 100644
--- a/src/Symfony/Bundle/SecurityBundle/Tests/DataCollector/SecurityDataCollectorTest.php
+++ b/src/Symfony/Bundle/SecurityBundle/Tests/DataCollector/SecurityDataCollectorTest.php
@@ -28,6 +28,7 @@
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Core\Authorization\TraceableAccessDecisionManager;
use Symfony\Component\Security\Core\Authorization\Voter\TraceableVoter;
+use Symfony\Component\Security\Core\Authorization\Voter\Vote;
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
use Symfony\Component\Security\Core\Role\RoleHierarchy;
use Symfony\Component\Security\Core\User\InMemoryUser;
@@ -464,4 +465,8 @@ final class DummyVoter implements VoterInterface
public function vote(TokenInterface $token, mixed $subject, array $attributes): int
{
}
+
+ public function getVote(TokenInterface $token, mixed $subject, array $attributes): Vote
+ {
+ }
}
diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/EventListener/VoteListenerTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/EventListener/VoteListenerTest.php
index d262effae29ac..b2bc95b50ec28 100644
--- a/src/Symfony/Bundle/SecurityBundle/Tests/EventListener/VoteListenerTest.php
+++ b/src/Symfony/Bundle/SecurityBundle/Tests/EventListener/VoteListenerTest.php
@@ -14,6 +14,7 @@
use PHPUnit\Framework\TestCase;
use Symfony\Bundle\SecurityBundle\EventListener\VoteListener;
use Symfony\Component\Security\Core\Authorization\TraceableAccessDecisionManager;
+use Symfony\Component\Security\Core\Authorization\Voter\Vote;
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
use Symfony\Component\Security\Core\Event\VoteEvent;
@@ -29,10 +30,33 @@ public function testOnVoterVote()
->onlyMethods(['addVoterVote'])
->getMock();
+ $vote = Vote::createGranted();
$traceableAccessDecisionManager
->expects($this->once())
->method('addVoterVote')
- ->with($voter, ['myattr1', 'myattr2'], VoterInterface::ACCESS_GRANTED);
+ ->with($voter, ['myattr1', 'myattr2'], $vote);
+
+ $sut = new VoteListener($traceableAccessDecisionManager);
+ $sut->onVoterVote(new VoteEvent($voter, 'mysubject', ['myattr1', 'myattr2'], $vote));
+ }
+
+ /**
+ * @group legacy
+ */
+ public function testOnVoterVoteLegacy()
+ {
+ $voter = $this->createMock(VoterInterface::class);
+
+ $traceableAccessDecisionManager = $this
+ ->getMockBuilder(TraceableAccessDecisionManager::class)
+ ->disableOriginalConstructor()
+ ->onlyMethods(['addVoterVote'])
+ ->getMock();
+
+ $traceableAccessDecisionManager
+ ->expects($this->once())
+ ->method('addVoterVote')
+ ->with($voter, ['myattr1', 'myattr2'], Vote::createGranted());
$sut = new VoteListener($traceableAccessDecisionManager);
$sut->onVoterVote(new VoteEvent($voter, 'mysubject', ['myattr1', 'myattr2'], VoterInterface::ACCESS_GRANTED));
diff --git a/src/Symfony/Bundle/SecurityBundle/composer.json b/src/Symfony/Bundle/SecurityBundle/composer.json
index 0ae91f9cfb023..6d24d503e980d 100644
--- a/src/Symfony/Bundle/SecurityBundle/composer.json
+++ b/src/Symfony/Bundle/SecurityBundle/composer.json
@@ -64,6 +64,7 @@
"symfony/framework-bundle": "<6.4",
"symfony/http-client": "<6.4",
"symfony/ldap": "<6.4",
+ "symfony/security-core": "<7.1",
"symfony/serializer": "<6.4",
"symfony/twig-bundle": "<6.4",
"symfony/validator": "<6.4"
diff --git a/src/Symfony/Component/Security/Core/Authorization/AccessDecision.php b/src/Symfony/Component/Security/Core/Authorization/AccessDecision.php
new file mode 100644
index 0000000000000..dc53d7467e8b0
--- /dev/null
+++ b/src/Symfony/Component/Security/Core/Authorization/AccessDecision.php
@@ -0,0 +1,108 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Security\Core\Authorization;
+
+use Symfony\Component\Security\Core\Authorization\Voter\Vote;
+use Symfony\Component\Security\Core\Authorization\Voter\Voter;
+use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
+
+/**
+ * An AccessDecision is returned by an AccessDecisionManager and contains the access verdict and all the related votes.
+ *
+ * @author Dany Maillard
+ */
+final class AccessDecision
+{
+ /**
+ * @param int $access One of the VoterInterface::ACCESS_* constants
+ * @param Vote[] $votes
+ */
+ private function __construct(private readonly int $access, private readonly array $votes = [])
+ {
+ }
+
+ public function getAccess(): int
+ {
+ return $this->access;
+ }
+
+ public function isGranted(): bool
+ {
+ return VoterInterface::ACCESS_GRANTED === $this->access;
+ }
+
+ public function isAbstain(): bool
+ {
+ return VoterInterface::ACCESS_ABSTAIN === $this->access;
+ }
+
+ public function isDenied(): bool
+ {
+ return VoterInterface::ACCESS_DENIED === $this->access;
+ }
+
+ /**
+ * @param Vote[] $votes
+ */
+ public static function createGranted(array $votes = []): self
+ {
+ return new self(VoterInterface::ACCESS_GRANTED, $votes);
+ }
+
+ /**
+ * @param Vote[] $votes
+ */
+ public static function createDenied(array $votes = []): self
+ {
+ return new self(VoterInterface::ACCESS_DENIED, $votes);
+ }
+
+ /**
+ * @return Vote[]
+ */
+ public function getVotes(): array
+ {
+ return $this->votes;
+ }
+
+ /**
+ * @return Vote[]
+ */
+ public function getGrantedVotes(): array
+ {
+ return $this->getVotesByAccess(Voter::ACCESS_GRANTED);
+ }
+
+ /**
+ * @return Vote[]
+ */
+ public function getAbstainedVotes(): array
+ {
+ return $this->getVotesByAccess(Voter::ACCESS_ABSTAIN);
+ }
+
+ /**
+ * @return Vote[]
+ */
+ public function getDeniedVotes(): array
+ {
+ return $this->getVotesByAccess(Voter::ACCESS_DENIED);
+ }
+
+ /**
+ * @return Vote[]
+ */
+ private function getVotesByAccess(int $access): array
+ {
+ return array_filter($this->votes, static fn (Vote $vote): bool => $vote->getAccess() === $access);
+ }
+}
diff --git a/src/Symfony/Component/Security/Core/Authorization/AccessDecisionManager.php b/src/Symfony/Component/Security/Core/Authorization/AccessDecisionManager.php
index 4a56f943a262d..8705b6728f8b0 100644
--- a/src/Symfony/Component/Security/Core/Authorization/AccessDecisionManager.php
+++ b/src/Symfony/Component/Security/Core/Authorization/AccessDecisionManager.php
@@ -15,6 +15,7 @@
use Symfony\Component\Security\Core\Authorization\Strategy\AccessDecisionStrategyInterface;
use Symfony\Component\Security\Core\Authorization\Strategy\AffirmativeStrategy;
use Symfony\Component\Security\Core\Authorization\Voter\CacheableVoterInterface;
+use Symfony\Component\Security\Core\Authorization\Voter\Vote;
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
use Symfony\Component\Security\Core\Exception\InvalidArgumentException;
@@ -26,12 +27,6 @@
*/
final class AccessDecisionManager implements AccessDecisionManagerInterface
{
- private const VALID_VOTES = [
- VoterInterface::ACCESS_GRANTED => true,
- VoterInterface::ACCESS_DENIED => true,
- VoterInterface::ACCESS_ABSTAIN => true,
- ];
-
private iterable $voters;
private array $votersCacheAttributes = [];
private array $votersCacheObject = [];
@@ -46,11 +41,35 @@ public function __construct(iterable $voters = [], ?AccessDecisionStrategyInterf
$this->strategy = $strategy ?? new AffirmativeStrategy();
}
+ public function getDecision(TokenInterface $token, array $attributes, mixed $object = null, bool $allowMultipleAttributes = false): AccessDecision
+ {
+ // Special case for AccessListener, do not remove the right side of the condition before 6.0
+ if (\count($attributes) > 1 && !$allowMultipleAttributes) {
+ throw new InvalidArgumentException(sprintf('Passing more than one Security attribute to "%s()" is not supported.', __METHOD__));
+ }
+
+ if (method_exists($this->strategy, 'getDecision')) {
+ $decision = $this->strategy->getDecision(
+ $this->collectVotes($token, $attributes, $object)
+ );
+ } else {
+ $decision = $this->strategy->decide(
+ $this->collectResults($token, $attributes, $object)
+ ) ? AccessDecision::createGranted() : AccessDecision::createDenied();
+ }
+
+ return $decision;
+ }
+
/**
* @param bool $allowMultipleAttributes Whether to allow passing multiple values to the $attributes array
+ *
+ * @deprecated since Symfony 7.1, use {@see getDecision()} instead.
*/
public function decide(TokenInterface $token, array $attributes, mixed $object = null, bool $allowMultipleAttributes = false): bool
{
+ trigger_deprecation('symfony/security-core', '7.1', 'Method "%s::decide()" has been deprecated, use "%s::getDecision()" instead.', __CLASS__, __CLASS__);
+
// Special case for AccessListener, do not remove the right side of the condition before 6.0
if (\count($attributes) > 1 && !$allowMultipleAttributes) {
throw new InvalidArgumentException(sprintf('Passing more than one Security attribute to "%s()" is not supported.', __METHOD__));
@@ -62,17 +81,33 @@ public function decide(TokenInterface $token, array $attributes, mixed $object =
}
/**
- * @return \Traversable
+ * @return \Traversable
*/
- private function collectResults(TokenInterface $token, array $attributes, mixed $object): \Traversable
+ private function collectVotes(TokenInterface $token, array $attributes, mixed $object): \Traversable
{
foreach ($this->getVoters($attributes, $object) as $voter) {
- $result = $voter->vote($token, $object, $attributes);
- if (!\is_int($result) || !(self::VALID_VOTES[$result] ?? false)) {
- 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)));
+ if (method_exists($voter, 'getVote')) {
+ yield $voter->getVote($token, $object, $attributes);
+ } else {
+ $result = $voter->vote($token, $object, $attributes);
+ yield match ($result) {
+ VoterInterface::ACCESS_GRANTED => Vote::createGranted(),
+ VoterInterface::ACCESS_DENIED => Vote::createDenied(),
+ VoterInterface::ACCESS_ABSTAIN => Vote::createAbstain(),
+ default => 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))),
+ };
}
+ }
+ }
- yield $result;
+ /**
+ * @return \Traversable
+ */
+ private function collectResults(TokenInterface $token, array $attributes, mixed $object): \Traversable
+ {
+ /** @var Vote $vote */
+ foreach ($this->collectVotes($token, $attributes, $object) as $vote) {
+ yield $vote->getAccess();
}
}
diff --git a/src/Symfony/Component/Security/Core/Authorization/AccessDecisionManagerInterface.php b/src/Symfony/Component/Security/Core/Authorization/AccessDecisionManagerInterface.php
index f25c7e1bef9b3..c2103fc6e55e0 100644
--- a/src/Symfony/Component/Security/Core/Authorization/AccessDecisionManagerInterface.php
+++ b/src/Symfony/Component/Security/Core/Authorization/AccessDecisionManagerInterface.php
@@ -17,6 +17,8 @@
* AccessDecisionManagerInterface makes authorization decisions.
*
* @author Fabien Potencier
+ *
+ * @method AccessDecision getDecision(TokenInterface $token, array $attributes, mixed $object = null)
*/
interface AccessDecisionManagerInterface
{
@@ -25,6 +27,8 @@ interface AccessDecisionManagerInterface
*
* @param array $attributes An array of attributes associated with the method being invoked
* @param mixed $object The object to secure
+ *
+ * @deprecated since Symfony 7.1, use {@see getDecision()} instead.
*/
public function decide(TokenInterface $token, array $attributes, mixed $object = null): bool;
}
diff --git a/src/Symfony/Component/Security/Core/Authorization/AuthorizationChecker.php b/src/Symfony/Component/Security/Core/Authorization/AuthorizationChecker.php
index c748697c494f9..7f77c7ba3413f 100644
--- a/src/Symfony/Component/Security/Core/Authorization/AuthorizationChecker.php
+++ b/src/Symfony/Component/Security/Core/Authorization/AuthorizationChecker.php
@@ -31,6 +31,11 @@ public function __construct(
}
final public function isGranted(mixed $attribute, mixed $subject = null): bool
+ {
+ return $this->getDecision($attribute, $subject)->isGranted();
+ }
+
+ final public function getDecision($attribute, $subject = null): AccessDecision
{
$token = $this->tokenStorage->getToken();
@@ -38,6 +43,12 @@ final public function isGranted(mixed $attribute, mixed $subject = null): bool
$token = new NullToken();
}
- return $this->accessDecisionManager->decide($token, [$attribute], $subject);
+ if (!method_exists($this->accessDecisionManager, 'getDecision')) {
+ trigger_deprecation('symfony/security-core', '7.1', 'Not implementing "%s::getDecision()" method is deprecated, and would be required in 7.0.', \get_class($this->accessDecisionManager));
+
+ return $this->accessDecisionManager->decide($token, [$attribute], $subject) ? AccessDecision::createGranted() : AccessDecision::createDenied();
+ }
+
+ return $this->accessDecisionManager->getDecision($token, [$attribute], $subject);
}
}
diff --git a/src/Symfony/Component/Security/Core/Authorization/AuthorizationCheckerInterface.php b/src/Symfony/Component/Security/Core/Authorization/AuthorizationCheckerInterface.php
index 6f5a6022178ba..0089b2b1dbf6e 100644
--- a/src/Symfony/Component/Security/Core/Authorization/AuthorizationCheckerInterface.php
+++ b/src/Symfony/Component/Security/Core/Authorization/AuthorizationCheckerInterface.php
@@ -15,6 +15,8 @@
* The AuthorizationCheckerInterface.
*
* @author Johannes M. Schmitt
+ *
+ * @method AccessDecision getDecision(mixed $attribute, mixed $subject = null)
*/
interface AuthorizationCheckerInterface
{
diff --git a/src/Symfony/Component/Security/Core/Authorization/Strategy/AccessDecisionStrategyInterface.php b/src/Symfony/Component/Security/Core/Authorization/Strategy/AccessDecisionStrategyInterface.php
index 00238378a30fa..619aacc625ed8 100644
--- a/src/Symfony/Component/Security/Core/Authorization/Strategy/AccessDecisionStrategyInterface.php
+++ b/src/Symfony/Component/Security/Core/Authorization/Strategy/AccessDecisionStrategyInterface.php
@@ -11,15 +11,21 @@
namespace Symfony\Component\Security\Core\Authorization\Strategy;
+use Symfony\Component\Security\Core\Authorization\AccessDecision;
+
/**
* A strategy for turning a stream of votes into a final decision.
*
* @author Alexander M. Turek
+ *
+ * @method AccessDecision getDecision(\Traversable $votes)
*/
interface AccessDecisionStrategyInterface
{
/**
* @param \Traversable $results
+ *
+ * @deprecated since Symfony 7.1, use {@see getDecision()} instead.
*/
public function decide(\Traversable $results): bool;
}
diff --git a/src/Symfony/Component/Security/Core/Authorization/Strategy/AffirmativeStrategy.php b/src/Symfony/Component/Security/Core/Authorization/Strategy/AffirmativeStrategy.php
index ecd74b208616c..e66db39a1b082 100644
--- a/src/Symfony/Component/Security/Core/Authorization/Strategy/AffirmativeStrategy.php
+++ b/src/Symfony/Component/Security/Core/Authorization/Strategy/AffirmativeStrategy.php
@@ -11,6 +11,8 @@
namespace Symfony\Component\Security\Core\Authorization\Strategy;
+use Symfony\Component\Security\Core\Authorization\AccessDecision;
+use Symfony\Component\Security\Core\Authorization\Voter\Vote;
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
/**
@@ -21,6 +23,7 @@
*
* @author Fabien Potencier
* @author Alexander M. Turek
+ * @author Antoine Lamirault
*/
final class AffirmativeStrategy implements AccessDecisionStrategyInterface, \Stringable
{
@@ -31,8 +34,37 @@ public function __construct(bool $allowIfAllAbstainDecisions = false)
$this->allowIfAllAbstainDecisions = $allowIfAllAbstainDecisions;
}
+ public function getDecision(\Traversable $votes): AccessDecision
+ {
+ $currentVotes = [];
+ $deny = 0;
+
+ /** @var Vote $vote */
+ foreach ($votes as $vote) {
+ $currentVotes[] = $vote;
+ if ($vote->isGranted()) {
+ return AccessDecision::createGranted($currentVotes);
+ }
+
+ if ($vote->isDenied()) {
+ ++$deny;
+ }
+ }
+
+ if ($deny > 0) {
+ return AccessDecision::createDenied($currentVotes);
+ }
+
+ return $this->allowIfAllAbstainDecisions
+ ? AccessDecision::createGranted($currentVotes)
+ : AccessDecision::createDenied($currentVotes)
+ ;
+ }
+
public function decide(\Traversable $results): bool
{
+ trigger_deprecation('symfony/security-core', '7.1', 'Method "%s::decide()" has been deprecated, use "%s::getDecision()" instead.', __CLASS__, __CLASS__);
+
$deny = 0;
foreach ($results as $result) {
if (VoterInterface::ACCESS_GRANTED === $result) {
diff --git a/src/Symfony/Component/Security/Core/Authorization/Strategy/ConsensusStrategy.php b/src/Symfony/Component/Security/Core/Authorization/Strategy/ConsensusStrategy.php
index 489b34287b520..6ec8f636e4596 100644
--- a/src/Symfony/Component/Security/Core/Authorization/Strategy/ConsensusStrategy.php
+++ b/src/Symfony/Component/Security/Core/Authorization/Strategy/ConsensusStrategy.php
@@ -11,6 +11,8 @@
namespace Symfony\Component\Security\Core\Authorization\Strategy;
+use Symfony\Component\Security\Core\Authorization\AccessDecision;
+use Symfony\Component\Security\Core\Authorization\Voter\Vote;
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
/**
@@ -29,6 +31,7 @@
*
* @author Fabien Potencier
* @author Alexander M. Turek
+ * @author Antoine Lamirault
*/
final class ConsensusStrategy implements AccessDecisionStrategyInterface, \Stringable
{
@@ -41,8 +44,47 @@ public function __construct(bool $allowIfAllAbstainDecisions = false, bool $allo
$this->allowIfEqualGrantedDeniedDecisions = $allowIfEqualGrantedDeniedDecisions;
}
+ public function getDecision(\Traversable $votes): AccessDecision
+ {
+ $currentVotes = [];
+
+ /** @var Vote $vote */
+ $grant = 0;
+ $deny = 0;
+ foreach ($votes as $vote) {
+ $currentVotes[] = $vote;
+ if ($vote->isGranted()) {
+ ++$grant;
+ } elseif ($vote->isDenied()) {
+ ++$deny;
+ }
+ }
+
+ if ($grant > $deny) {
+ return AccessDecision::createGranted($currentVotes);
+ }
+
+ if ($deny > $grant) {
+ return AccessDecision::createDenied($currentVotes);
+ }
+
+ if ($grant > 0) {
+ return $this->allowIfEqualGrantedDeniedDecisions
+ ? AccessDecision::createGranted($currentVotes)
+ : AccessDecision::createDenied($currentVotes)
+ ;
+ }
+
+ return $this->allowIfAllAbstainDecisions
+ ? AccessDecision::createGranted($currentVotes)
+ : AccessDecision::createDenied($currentVotes)
+ ;
+ }
+
public function decide(\Traversable $results): bool
{
+ trigger_deprecation('symfony/security-core', '7.1', 'Method "%s::decide()" has been deprecated, use "%s::getDecision()" instead.', __CLASS__, __CLASS__);
+
$grant = 0;
$deny = 0;
foreach ($results as $result) {
diff --git a/src/Symfony/Component/Security/Core/Authorization/Strategy/PriorityStrategy.php b/src/Symfony/Component/Security/Core/Authorization/Strategy/PriorityStrategy.php
index 9599950c7fdbb..61d0ecd868728 100644
--- a/src/Symfony/Component/Security/Core/Authorization/Strategy/PriorityStrategy.php
+++ b/src/Symfony/Component/Security/Core/Authorization/Strategy/PriorityStrategy.php
@@ -11,6 +11,8 @@
namespace Symfony\Component\Security\Core\Authorization\Strategy;
+use Symfony\Component\Security\Core\Authorization\AccessDecision;
+use Symfony\Component\Security\Core\Authorization\Voter\Vote;
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
/**
@@ -22,6 +24,7 @@
*
* @author Fabien Potencier
* @author Alexander M. Turek
+ * @author Antoine Lamirault
*/
final class PriorityStrategy implements AccessDecisionStrategyInterface, \Stringable
{
@@ -32,8 +35,32 @@ public function __construct(bool $allowIfAllAbstainDecisions = false)
$this->allowIfAllAbstainDecisions = $allowIfAllAbstainDecisions;
}
+ public function getDecision(\Traversable $votes): AccessDecision
+ {
+ $currentVotes = [];
+
+ /** @var Vote $vote */
+ foreach ($votes as $vote) {
+ $currentVotes[] = $vote;
+ if ($vote->isGranted()) {
+ return AccessDecision::createGranted($currentVotes);
+ }
+
+ if ($vote->isDenied()) {
+ return AccessDecision::createDenied($currentVotes);
+ }
+ }
+
+ return $this->allowIfAllAbstainDecisions
+ ? AccessDecision::createGranted($currentVotes)
+ : AccessDecision::createDenied($currentVotes)
+ ;
+ }
+
public function decide(\Traversable $results): bool
{
+ trigger_deprecation('symfony/security-core', '7.1', 'Method "%s::decide()" has been deprecated, use "%s::getDecision()" instead.', __CLASS__, __CLASS__);
+
foreach ($results as $result) {
if (VoterInterface::ACCESS_GRANTED === $result) {
return true;
diff --git a/src/Symfony/Component/Security/Core/Authorization/Strategy/UnanimousStrategy.php b/src/Symfony/Component/Security/Core/Authorization/Strategy/UnanimousStrategy.php
index 1f3b85c548fbf..c5b41bdbe12e5 100644
--- a/src/Symfony/Component/Security/Core/Authorization/Strategy/UnanimousStrategy.php
+++ b/src/Symfony/Component/Security/Core/Authorization/Strategy/UnanimousStrategy.php
@@ -11,6 +11,8 @@
namespace Symfony\Component\Security\Core\Authorization\Strategy;
+use Symfony\Component\Security\Core\Authorization\AccessDecision;
+use Symfony\Component\Security\Core\Authorization\Voter\Vote;
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
/**
@@ -21,6 +23,7 @@
*
* @author Fabien Potencier
* @author Alexander M. Turek
+ * @author Antoine Lamirault
*/
final class UnanimousStrategy implements AccessDecisionStrategyInterface, \Stringable
{
@@ -31,8 +34,38 @@ public function __construct(bool $allowIfAllAbstainDecisions = false)
$this->allowIfAllAbstainDecisions = $allowIfAllAbstainDecisions;
}
+ public function getDecision(\Traversable $votes): AccessDecision
+ {
+ $currentVotes = [];
+ $grant = 0;
+
+ /** @var Vote $vote */
+ foreach ($votes as $vote) {
+ $currentVotes[] = $vote;
+ if ($vote->isDenied()) {
+ return AccessDecision::createDenied($currentVotes);
+ }
+
+ if ($vote->isGranted()) {
+ ++$grant;
+ }
+ }
+
+ // no deny votes
+ if ($grant > 0) {
+ return AccessDecision::createGranted($currentVotes);
+ }
+
+ return $this->allowIfAllAbstainDecisions
+ ? AccessDecision::createGranted($currentVotes)
+ : AccessDecision::createDenied($currentVotes)
+ ;
+ }
+
public function decide(\Traversable $results): bool
{
+ trigger_deprecation('symfony/security-core', '7.1', 'Method "%s::decide()" has been deprecated, use "%s::getDecision()" instead.', __CLASS__, __CLASS__);
+
$grant = 0;
foreach ($results as $result) {
if (VoterInterface::ACCESS_DENIED === $result) {
diff --git a/src/Symfony/Component/Security/Core/Authorization/TraceableAccessDecisionManager.php b/src/Symfony/Component/Security/Core/Authorization/TraceableAccessDecisionManager.php
index cb44dce4c0f0d..db70327955c2b 100644
--- a/src/Symfony/Component/Security/Core/Authorization/TraceableAccessDecisionManager.php
+++ b/src/Symfony/Component/Security/Core/Authorization/TraceableAccessDecisionManager.php
@@ -13,6 +13,7 @@
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Strategy\AccessDecisionStrategyInterface;
+use Symfony\Component\Security\Core\Authorization\Voter\Vote;
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
/**
@@ -47,8 +48,29 @@ public function __construct(AccessDecisionManagerInterface $manager)
}
}
+ public function getDecision(TokenInterface $token, array $attributes, mixed $object = null, bool $allowMultipleAttributes = false): AccessDecision
+ {
+ $currentDecisionLog = [
+ 'attributes' => $attributes,
+ 'object' => $object,
+ 'voterDetails' => [],
+ ];
+
+ $this->currentLog[] = &$currentDecisionLog;
+
+ $result = $this->manager->getDecision($token, $attributes, $object, $allowMultipleAttributes);
+
+ $currentDecisionLog['result'] = $result;
+
+ $this->decisionLog[] = array_pop($this->currentLog); // Using a stack since getDecision can be called by voters
+
+ return $result;
+ }
+
public function decide(TokenInterface $token, array $attributes, mixed $object = null, bool $allowMultipleAttributes = false): bool
{
+ trigger_deprecation('symfony/security-core', '7.1', 'Method "%s::decide()" has been deprecated, use "%s::getDecision()" instead.', __CLASS__, __CLASS__);
+
$currentDecisionLog = [
'attributes' => $attributes,
'object' => $object,
@@ -69,11 +91,16 @@ public function decide(TokenInterface $token, array $attributes, mixed $object =
/**
* Adds voter vote and class to the voter details.
*
- * @param array $attributes attributes used for the vote
- * @param int $vote vote of the voter
+ * @param array $attributes attributes used for the vote
+ * @param Vote|int $vote vote of the voter
*/
- public function addVoterVote(VoterInterface $voter, array $attributes, int $vote): void
+ public function addVoterVote(VoterInterface $voter, array $attributes, Vote|int $vote): void
{
+ if (!$vote instanceof Vote) {
+ trigger_deprecation('symfony/security-core', '7.1', 'Passing an int as the third argument to "%s::addVoterVote()" is deprecated, pass an instance of "%s" instead.', __CLASS__, Vote::class);
+ $vote = new Vote($vote);
+ }
+
$currentLogIndex = \count($this->currentLog) - 1;
$this->currentLog[$currentLogIndex]['voterDetails'][] = [
'voter' => $voter,
diff --git a/src/Symfony/Component/Security/Core/Authorization/Voter/AuthenticatedVoter.php b/src/Symfony/Component/Security/Core/Authorization/Voter/AuthenticatedVoter.php
index d7b2b22431b04..39d4e145ee704 100644
--- a/src/Symfony/Component/Security/Core/Authorization/Voter/AuthenticatedVoter.php
+++ b/src/Symfony/Component/Security/Core/Authorization/Voter/AuthenticatedVoter.php
@@ -40,13 +40,13 @@ public function __construct(AuthenticationTrustResolverInterface $authentication
$this->authenticationTrustResolver = $authenticationTrustResolver;
}
- public function vote(TokenInterface $token, mixed $subject, array $attributes): int
+ public function getVote(TokenInterface $token, mixed $subject, array $attributes): Vote
{
if ($attributes === [self::PUBLIC_ACCESS]) {
- return VoterInterface::ACCESS_GRANTED;
+ return Vote::createGranted();
}
- $result = VoterInterface::ACCESS_ABSTAIN;
+ $result = Vote::createAbstain();
foreach ($attributes as $attribute) {
if (null === $attribute || (self::IS_AUTHENTICATED_FULLY !== $attribute
&& self::IS_AUTHENTICATED_REMEMBERED !== $attribute
@@ -56,35 +56,42 @@ public function vote(TokenInterface $token, mixed $subject, array $attributes):
continue;
}
- $result = VoterInterface::ACCESS_DENIED;
+ $result = Vote::createDenied();
if (self::IS_AUTHENTICATED_FULLY === $attribute
&& $this->authenticationTrustResolver->isFullFledged($token)) {
- return VoterInterface::ACCESS_GRANTED;
+ return Vote::createGranted();
}
if (self::IS_AUTHENTICATED_REMEMBERED === $attribute
&& ($this->authenticationTrustResolver->isRememberMe($token)
|| $this->authenticationTrustResolver->isFullFledged($token))) {
- return VoterInterface::ACCESS_GRANTED;
+ return Vote::createGranted();
}
if (self::IS_AUTHENTICATED === $attribute && $this->authenticationTrustResolver->isAuthenticated($token)) {
- return VoterInterface::ACCESS_GRANTED;
+ return Vote::createGranted();
}
if (self::IS_REMEMBERED === $attribute && $this->authenticationTrustResolver->isRememberMe($token)) {
- return VoterInterface::ACCESS_GRANTED;
+ return Vote::createGranted();
}
if (self::IS_IMPERSONATOR === $attribute && $token instanceof SwitchUserToken) {
- return VoterInterface::ACCESS_GRANTED;
+ return Vote::createGranted();
}
}
return $result;
}
+ public function vote(TokenInterface $token, mixed $subject, array $attributes): int
+ {
+ trigger_deprecation('symfony/security-core', '7.1', 'Method "%s::vote()" has been deprecated, use "%s::getVote()" instead.', __CLASS__, __CLASS__);
+
+ return $this->getVote($token, $subject, $attributes)->getAccess();
+ }
+
public function supportsAttribute(string $attribute): bool
{
return \in_array($attribute, [
diff --git a/src/Symfony/Component/Security/Core/Authorization/Voter/ExpressionVoter.php b/src/Symfony/Component/Security/Core/Authorization/Voter/ExpressionVoter.php
index 6de9c95465d0a..b53eb6a35969e 100644
--- a/src/Symfony/Component/Security/Core/Authorization/Voter/ExpressionVoter.php
+++ b/src/Symfony/Component/Security/Core/Authorization/Voter/ExpressionVoter.php
@@ -49,9 +49,9 @@ public function supportsType(string $subjectType): bool
return true;
}
- public function vote(TokenInterface $token, mixed $subject, array $attributes): int
+ public function getVote(TokenInterface $token, mixed $subject, array $attributes): Vote
{
- $result = VoterInterface::ACCESS_ABSTAIN;
+ $result = Vote::createAbstain();
$variables = null;
foreach ($attributes as $attribute) {
if (!$attribute instanceof Expression) {
@@ -60,15 +60,22 @@ public function vote(TokenInterface $token, mixed $subject, array $attributes):
$variables ??= $this->getVariables($token, $subject);
- $result = VoterInterface::ACCESS_DENIED;
+ $result = Vote::createDenied();
if ($this->expressionLanguage->evaluate($attribute, $variables)) {
- return VoterInterface::ACCESS_GRANTED;
+ return Vote::createGranted();
}
}
return $result;
}
+ public function vote(TokenInterface $token, mixed $subject, array $attributes): int
+ {
+ trigger_deprecation('symfony/security-core', '7.1', 'Method "%s::vote()" has been deprecated, use "%s::getVote()" instead.', __CLASS__, __CLASS__);
+
+ return $this->getVote($token, $subject, $attributes)->getAccess();
+ }
+
private function getVariables(TokenInterface $token, mixed $subject): array
{
$roleNames = $token->getRoleNames();
diff --git a/src/Symfony/Component/Security/Core/Authorization/Voter/RoleVoter.php b/src/Symfony/Component/Security/Core/Authorization/Voter/RoleVoter.php
index 76de3a32c7f3a..bcecec2e751f0 100644
--- a/src/Symfony/Component/Security/Core/Authorization/Voter/RoleVoter.php
+++ b/src/Symfony/Component/Security/Core/Authorization/Voter/RoleVoter.php
@@ -27,9 +27,9 @@ public function __construct(string $prefix = 'ROLE_')
$this->prefix = $prefix;
}
- public function vote(TokenInterface $token, mixed $subject, array $attributes): int
+ public function getVote(TokenInterface $token, mixed $subject, array $attributes): Vote
{
- $result = VoterInterface::ACCESS_ABSTAIN;
+ $result = Vote::createAbstain();
$roles = $this->extractRoles($token);
foreach ($attributes as $attribute) {
@@ -37,15 +37,22 @@ public function vote(TokenInterface $token, mixed $subject, array $attributes):
continue;
}
- $result = VoterInterface::ACCESS_DENIED;
+ $result = Vote::createDenied();
if (\in_array($attribute, $roles, true)) {
- return VoterInterface::ACCESS_GRANTED;
+ return Vote::createGranted();
}
}
return $result;
}
+ public function vote(TokenInterface $token, mixed $subject, array $attributes): int
+ {
+ trigger_deprecation('symfony/security-core', '7.1', 'Method "%s::vote()" has been deprecated, use "%s::getVote()" instead.', __CLASS__, __CLASS__);
+
+ return $this->getVote($token, $subject, $attributes)->getAccess();
+ }
+
public function supportsAttribute(string $attribute): bool
{
return str_starts_with($attribute, $this->prefix);
diff --git a/src/Symfony/Component/Security/Core/Authorization/Voter/TraceableVoter.php b/src/Symfony/Component/Security/Core/Authorization/Voter/TraceableVoter.php
index 412bb9760bfec..fa43b292f182f 100644
--- a/src/Symfony/Component/Security/Core/Authorization/Voter/TraceableVoter.php
+++ b/src/Symfony/Component/Security/Core/Authorization/Voter/TraceableVoter.php
@@ -33,13 +33,25 @@ public function __construct(VoterInterface $voter, EventDispatcherInterface $eve
$this->eventDispatcher = $eventDispatcher;
}
- public function vote(TokenInterface $token, mixed $subject, array $attributes): int
+ public function getVote(TokenInterface $token, mixed $subject, array $attributes): Vote
{
- $result = $this->voter->vote($token, $subject, $attributes);
+ if (method_exists($this->voter, 'getVote')) {
+ $vote = $this->voter->getVote($token, $subject, $attributes);
+ } else {
+ $result = $this->voter->vote($token, $subject, $attributes);
+ $vote = new Vote($result);
+ }
+
+ $this->eventDispatcher->dispatch(new VoteEvent($this->voter, $subject, $attributes, $vote), 'debug.security.authorization.vote');
+
+ return $vote;
+ }
- $this->eventDispatcher->dispatch(new VoteEvent($this->voter, $subject, $attributes, $result), 'debug.security.authorization.vote');
+ public function vote(TokenInterface $token, mixed $subject, array $attributes): int
+ {
+ trigger_deprecation('symfony/security-core', '7.1', 'Method "%s::vote()" has been deprecated, use "%s::getVote()" instead.', __CLASS__, __CLASS__);
- return $result;
+ return $this->getVote($token, $subject, $attributes)->getAccess();
}
public function getDecoratedVoter(): VoterInterface
diff --git a/src/Symfony/Component/Security/Core/Authorization/Voter/Vote.php b/src/Symfony/Component/Security/Core/Authorization/Voter/Vote.php
new file mode 100644
index 0000000000000..378dbca6f0463
--- /dev/null
+++ b/src/Symfony/Component/Security/Core/Authorization/Voter/Vote.php
@@ -0,0 +1,134 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Security\Core\Authorization\Voter;
+
+use Symfony\Component\Security\Core\Exception\InvalidArgumentException;
+
+/**
+ * A Vote is returned by a Voter and contains the access (granted, abstain or denied).
+ * It can also contain one or multiple messages explaining the vote decision.
+ *
+ * @author Dany Maillard
+ * @author Antoine Lamirault
+ */
+final class Vote
+{
+ /**
+ * @var int One of the VoterInterface::ACCESS_* constants
+ */
+ private int $access;
+
+ /**
+ * @var string[]
+ */
+ private array $messages;
+
+ private array $context;
+
+ /**
+ * @param int $access One of the VoterInterface::ACCESS_* constants
+ */
+ public function __construct(int $access, string|array $messages = [], array $context = [])
+ {
+ $this->access = $access;
+ $this->setMessages($messages);
+ $this->context = $context;
+ }
+
+ public function getAccess(): int
+ {
+ return $this->access;
+ }
+
+ public function isGranted(): bool
+ {
+ return VoterInterface::ACCESS_GRANTED === $this->access;
+ }
+
+ public function isAbstain(): bool
+ {
+ return VoterInterface::ACCESS_ABSTAIN === $this->access;
+ }
+
+ public function isDenied(): bool
+ {
+ return VoterInterface::ACCESS_DENIED === $this->access;
+ }
+
+ /**
+ * @param string|string[] $messages
+ */
+ public static function create(int $access, string|array $messages = [], array $context = []): self
+ {
+ return new self($access, $messages, $context);
+ }
+
+ /**
+ * @param string|string[] $messages
+ */
+ public static function createGranted(string|array $messages = [], array $context = []): self
+ {
+ return new self(VoterInterface::ACCESS_GRANTED, $messages, $context);
+ }
+
+ /**
+ * @param string|string[] $messages
+ */
+ public static function createAbstain(string|array $messages = [], array $context = []): self
+ {
+ return new self(VoterInterface::ACCESS_ABSTAIN, $messages, $context);
+ }
+
+ /**
+ * @param string|string[] $messages
+ */
+ public static function createDenied(string|array $messages = [], array $context = []): self
+ {
+ return new self(VoterInterface::ACCESS_DENIED, $messages, $context);
+ }
+
+ /**
+ * @param string|string[] $messages
+ */
+ public function setMessages(string|array $messages): void
+ {
+ $this->messages = (array) $messages;
+ foreach ($this->messages as $message) {
+ if (!\is_string($message)) {
+ throw new InvalidArgumentException(sprintf('Message must be string, "%s" given.', get_debug_type($message)));
+ }
+ }
+ }
+
+ public function addMessage(string $message): void
+ {
+ $this->messages[] = $message;
+ }
+
+ /**
+ * @return string[]
+ */
+ public function getMessages(): array
+ {
+ return $this->messages;
+ }
+
+ public function getMessage(): string
+ {
+ return implode(', ', $this->messages);
+ }
+
+ public function getContext(): array
+ {
+ return $this->context;
+ }
+}
diff --git a/src/Symfony/Component/Security/Core/Authorization/Voter/Voter.php b/src/Symfony/Component/Security/Core/Authorization/Voter/Voter.php
index 1f76a42eaf1b8..8364ceddfc751 100644
--- a/src/Symfony/Component/Security/Core/Authorization/Voter/Voter.php
+++ b/src/Symfony/Component/Security/Core/Authorization/Voter/Voter.php
@@ -24,10 +24,10 @@
*/
abstract class Voter implements VoterInterface, CacheableVoterInterface
{
- public function vote(TokenInterface $token, mixed $subject, array $attributes): int
+ public function getVote(TokenInterface $token, mixed $subject, array $attributes): Vote
{
// abstain vote by default in case none of the attributes are supported
- $vote = self::ACCESS_ABSTAIN;
+ $vote = $this->abstain();
foreach ($attributes as $attribute) {
try {
@@ -43,17 +43,66 @@ public function vote(TokenInterface $token, mixed $subject, array $attributes):
}
// as soon as at least one attribute is supported, default is to deny access
- $vote = self::ACCESS_DENIED;
+ if (!$vote->isDenied()) {
+ $vote = $this->deny();
+ }
+
+ $decision = $this->voteOnAttribute($attribute, $subject, $token);
+ if (\is_bool($decision)) {
+ trigger_deprecation('symfony/security-core', '7.1', 'Returning a boolean in "%s::voteOnAttribute()" is deprecated, return an instance of "%s" instead.', static::class, Vote::class);
+ $decision = $decision ? $this->grant() : $this->deny();
+ }
- if ($this->voteOnAttribute($attribute, $subject, $token)) {
+ if ($decision->isGranted()) {
// grant access as soon as at least one attribute returns a positive response
- return self::ACCESS_GRANTED;
+ return $decision;
+ }
+
+ if ('' !== $decisionMessage = $decision->getMessage()) {
+ $vote->addMessage($decisionMessage);
}
}
return $vote;
}
+ public function vote(TokenInterface $token, mixed $subject, array $attributes): int
+ {
+ trigger_deprecation('symfony/security-core', '7.1', 'Method "%s::vote()" has been deprecated, use "%s::getVote()" instead.', __CLASS__, __CLASS__);
+
+ return $this->getVote($token, $subject, $attributes)->getAccess();
+ }
+
+ /**
+ * Creates a granted vote.
+ *
+ * @param string|string[] $messages
+ */
+ protected function grant(string|array $messages = [], array $context = []): Vote
+ {
+ return Vote::createGranted($messages, $context);
+ }
+
+ /**
+ * Creates an abstained vote.
+ *
+ * @param string|string[] $messages
+ */
+ protected function abstain(string|array $messages = [], array $context = []): Vote
+ {
+ return Vote::createAbstain($messages, $context);
+ }
+
+ /**
+ * Creates a denied vote.
+ *
+ * @param string|string[] $messages
+ */
+ protected function deny(string|array $messages = [], array $context = []): Vote
+ {
+ return Vote::createDenied($messages, $context);
+ }
+
/**
* Return false if your voter doesn't support the given attribute. Symfony will cache
* that decision and won't call your voter again for that attribute.
@@ -90,6 +139,8 @@ abstract protected function supports(string $attribute, mixed $subject): bool;
*
* @param TAttribute $attribute
* @param TSubject $subject
+ *
+ * @return Vote|bool Returning a boolean is deprecated since Symfony 7.1. Return a Vote object instead.
*/
- abstract protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool;
+ abstract protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): Vote|bool;
}
diff --git a/src/Symfony/Component/Security/Core/Authorization/Voter/VoterInterface.php b/src/Symfony/Component/Security/Core/Authorization/Voter/VoterInterface.php
index 5255c88e6ec0f..1daf112f3b921 100644
--- a/src/Symfony/Component/Security/Core/Authorization/Voter/VoterInterface.php
+++ b/src/Symfony/Component/Security/Core/Authorization/Voter/VoterInterface.php
@@ -17,6 +17,8 @@
* VoterInterface is the interface implemented by all voters.
*
* @author Fabien Potencier
+ *
+ * @method Vote getVote(TokenInterface $token, mixed $subject, array $attributes)
*/
interface VoterInterface
{
@@ -34,6 +36,8 @@ interface VoterInterface
* @param array $attributes An array of attributes associated with the method being invoked
*
* @return self::ACCESS_*
+ *
+ * @deprecated since Symfony 7.1, use {@see getVote()} instead.
*/
public function vote(TokenInterface $token, mixed $subject, array $attributes): int;
}
diff --git a/src/Symfony/Component/Security/Core/CHANGELOG.md b/src/Symfony/Component/Security/Core/CHANGELOG.md
index 47b4a21082738..7ecbfc2a5d1de 100644
--- a/src/Symfony/Component/Security/Core/CHANGELOG.md
+++ b/src/Symfony/Component/Security/Core/CHANGELOG.md
@@ -2,6 +2,20 @@ CHANGELOG
=========
+7.1
+---
+
+ * Add method `getDecision()` to `AccessDecisionStrategyInterface`
+ * Deprecate `AccessDecisionStrategyInterface::decide()` in favor of `AccessDecisionStrategyInterface::getDecision()`
+ * Add method `getVote()` to `VoterInterface`
+ * Deprecate `VoterInterface::vote()` in favor of `AccessDecisionStrategyInterface::getVote()`
+ * Deprecate returning `bool` from `Voter::voteOnAttribute()` (it must return a `Vote`)
+ * Add method `getDecision()` to `AccessDecisionManagerInterface`
+ * Deprecate `AccessDecisionManagerInterface::decide()` in favor of `AccessDecisionManagerInterface::getDecision()`
+ * Add method `getDecision()` to `AuthorizationCheckerInterface`
+ * Add methods `setAccessDecision()` and `getAccessDecision()` to `AccessDeniedException`
+ * Add method `getDecision()` to `Security`
+
7.0
---
diff --git a/src/Symfony/Component/Security/Core/Event/VoteEvent.php b/src/Symfony/Component/Security/Core/Event/VoteEvent.php
index 1b1d6a336d6a1..cc72ba32edfd7 100644
--- a/src/Symfony/Component/Security/Core/Event/VoteEvent.php
+++ b/src/Symfony/Component/Security/Core/Event/VoteEvent.php
@@ -11,6 +11,7 @@
namespace Symfony\Component\Security\Core\Event;
+use Symfony\Component\Security\Core\Authorization\Voter\Vote;
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
use Symfony\Contracts\EventDispatcher\Event;
@@ -26,13 +27,19 @@ final class VoteEvent extends Event
private VoterInterface $voter;
private mixed $subject;
private array $attributes;
- private int $vote;
+ private Vote $vote;
- public function __construct(VoterInterface $voter, mixed $subject, array $attributes, int $vote)
+ public function __construct(VoterInterface $voter, mixed $subject, array $attributes, Vote|int $vote)
{
$this->voter = $voter;
$this->subject = $subject;
$this->attributes = $attributes;
+
+ if (!$vote instanceof Vote) {
+ trigger_deprecation('symfony/security-core', '7.1', 'Passing an int as the fourth argument to "%s::__construct" is deprecated, pass a "%s" instance instead.', __CLASS__, Vote::class);
+
+ $vote = new Vote($vote);
+ }
$this->vote = $vote;
}
@@ -51,7 +58,17 @@ public function getAttributes(): array
return $this->attributes;
}
+ /**
+ * @deprecated since Symfony 7.1, use {@see getVoteDecision()} instead.
+ */
public function getVote(): int
+ {
+ trigger_deprecation('symfony/security-core', '7.1', 'Method "%s::getVote()" has been deprecated, use "%s::getVoteDecision()" instead.', __CLASS__, __CLASS__);
+
+ return $this->vote->getAccess();
+ }
+
+ public function getVoteDecision(): Vote
{
return $this->vote;
}
diff --git a/src/Symfony/Component/Security/Core/Exception/AccessDeniedException.php b/src/Symfony/Component/Security/Core/Exception/AccessDeniedException.php
index 93c3869470d05..151312350b579 100644
--- a/src/Symfony/Component/Security/Core/Exception/AccessDeniedException.php
+++ b/src/Symfony/Component/Security/Core/Exception/AccessDeniedException.php
@@ -12,6 +12,8 @@
namespace Symfony\Component\Security\Core\Exception;
use Symfony\Component\HttpKernel\Attribute\WithHttpStatus;
+use Symfony\Component\Security\Core\Authorization\AccessDecision;
+use Symfony\Component\Security\Core\Authorization\Voter\Vote;
/**
* AccessDeniedException is thrown when the account has not the required role.
@@ -23,6 +25,7 @@ class AccessDeniedException extends RuntimeException
{
private array $attributes = [];
private mixed $subject = null;
+ private ?AccessDecision $accessDecision = null;
public function __construct(string $message = 'Access Denied.', ?\Throwable $previous = null, int $code = 403)
{
@@ -48,4 +51,29 @@ public function setSubject(mixed $subject): void
{
$this->subject = $subject;
}
+
+ /**
+ * Sets an access decision and appends the denied reasons to the exception message.
+ */
+ public function setAccessDecision(AccessDecision $accessDecision): void
+ {
+ $this->accessDecision = $accessDecision;
+ if (!$deniedVotes = $accessDecision->getDeniedVotes()) {
+ return;
+ }
+
+ $messages = array_map(static fn (Vote $vote): string => sprintf('"%s"', $vote->getMessage()), $deniedVotes);
+
+ if ($messages) {
+ $this->message .= sprintf(' Decision message%s %s', \count($messages) > 1 ? 's are' : ' is', implode(' and ', $messages));
+ }
+ }
+
+ /**
+ * Gets the access decision.
+ */
+ public function getAccessDecision(): ?AccessDecision
+ {
+ return $this->accessDecision;
+ }
}
diff --git a/src/Symfony/Component/Security/Core/Test/AccessDecisionStrategyTestCase.php b/src/Symfony/Component/Security/Core/Test/AccessDecisionStrategyTestCase.php
index bf2a2b9a15ec3..b6a9db7bdee51 100644
--- a/src/Symfony/Component/Security/Core/Test/AccessDecisionStrategyTestCase.php
+++ b/src/Symfony/Component/Security/Core/Test/AccessDecisionStrategyTestCase.php
@@ -13,8 +13,10 @@
use PHPUnit\Framework\TestCase;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
+use Symfony\Component\Security\Core\Authorization\AccessDecision;
use Symfony\Component\Security\Core\Authorization\AccessDecisionManager;
use Symfony\Component\Security\Core\Authorization\Strategy\AccessDecisionStrategyInterface;
+use Symfony\Component\Security\Core\Authorization\Voter\Vote;
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
/**
@@ -29,12 +31,27 @@ abstract class AccessDecisionStrategyTestCase extends TestCase
*
* @param VoterInterface[] $voters
*/
- final public function testDecide(AccessDecisionStrategyInterface $strategy, array $voters, bool $expected)
+ final public function testGetDecision(AccessDecisionStrategyInterface $strategy, array $voters, AccessDecision $expected)
{
$token = $this->createMock(TokenInterface::class);
$manager = new AccessDecisionManager($voters, $strategy);
- $this->assertSame($expected, $manager->decide($token, ['ROLE_FOO']));
+ $this->assertEquals($expected, $manager->getDecision($token, ['ROLE_FOO']));
+ }
+
+ /**
+ * @group legacy
+ *
+ * @dataProvider provideStrategyTests
+ *
+ * @param VoterInterface[] $voters
+ */
+ final public function testDecideLegacy(AccessDecisionStrategyInterface $strategy, array $voters, AccessDecision $expected)
+ {
+ $token = $this->createMock(TokenInterface::class);
+ $manager = new AccessDecisionManager($voters, $strategy);
+
+ $this->assertSame($expected->isGranted(), $manager->decide($token, ['ROLE_FOO']));
}
/**
@@ -75,6 +92,11 @@ public function vote(TokenInterface $token, $subject, array $attributes): int
{
return $this->vote;
}
+
+ public function getVote(TokenInterface $token, mixed $subject, array $attributes): Vote
+ {
+ return new Vote($this->vote);
+ }
};
}
}
diff --git a/src/Symfony/Component/Security/Core/Tests/Authorization/AccessDecisionManagerTest.php b/src/Symfony/Component/Security/Core/Tests/Authorization/AccessDecisionManagerTest.php
index 37848ab7a9ee4..fcfeb1c0ea644 100644
--- a/src/Symfony/Component/Security/Core/Tests/Authorization/AccessDecisionManagerTest.php
+++ b/src/Symfony/Component/Security/Core/Tests/Authorization/AccessDecisionManagerTest.php
@@ -14,9 +14,11 @@
use PHPUnit\Framework\Assert;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
+use Symfony\Component\Security\Core\Authorization\AccessDecision;
use Symfony\Component\Security\Core\Authorization\AccessDecisionManager;
use Symfony\Component\Security\Core\Authorization\Strategy\AccessDecisionStrategyInterface;
use Symfony\Component\Security\Core\Authorization\Voter\CacheableVoterInterface;
+use Symfony\Component\Security\Core\Authorization\Voter\Vote;
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
class AccessDecisionManagerTest extends TestCase
@@ -34,8 +36,54 @@ public function testVoterCalls()
$token = $this->createMock(TokenInterface::class);
$voters = [
- $this->getExpectedVoter(VoterInterface::ACCESS_DENIED),
- $this->getExpectedVoter(VoterInterface::ACCESS_GRANTED),
+ $this->getExpectedVoter(Vote::createDenied()),
+ $this->getExpectedVoter(Vote::createGranted()),
+ $this->getUnexpectedVoter(),
+ ];
+
+ $strategy = new class() implements AccessDecisionStrategyInterface {
+ public function getDecision(\Traversable $votes): AccessDecision
+ {
+ $i = 0;
+ /** @var Vote $vote */
+ foreach ($votes as $vote) {
+ switch ($i++) {
+ case 0:
+ Assert::assertSame(VoterInterface::ACCESS_DENIED, $vote->getAccess());
+
+ break;
+ case 1:
+ Assert::assertSame(VoterInterface::ACCESS_GRANTED, $vote->getAccess());
+
+ return AccessDecision::createGranted();
+ }
+ }
+
+ return AccessDecision::createDenied();
+ }
+
+ public function decide(\Traversable $results): bool
+ {
+ throw new \RuntimeException('Method should not be called');
+ }
+ };
+
+ $manager = new AccessDecisionManager($voters, $strategy);
+
+ $expectedDecision = AccessDecision::createGranted();
+ $this->assertEquals($expectedDecision, $manager->getDecision($token, ['ROLE_FOO']));
+ }
+
+ /**
+ * @group legacy
+ */
+ public function testVoterCallsLegacy()
+ {
+ $token = $this->createMock(TokenInterface::class);
+
+ $voters = [
+ $this->getExpectedVoterLegacy(VoterInterface::ACCESS_DENIED),
+ $this->getExpectedVoterLegacy(VoterInterface::ACCESS_GRANTED),
$this->getUnexpectedVoter(),
];
@@ -68,7 +116,11 @@ public function decide(\Traversable $results): bool
public function testCacheableVoters()
{
$token = $this->createMock(TokenInterface::class);
- $voter = $this->getMockBuilder(CacheableVoterInterface::class)->getMockForAbstractClass();
+ $voter = $this
+ ->getMockBuilder(CacheableVoterInterface::class)
+ ->onlyMethods(['supportsAttribute', 'supportsType', 'vote'])
+ ->addMethods(['getVote'])
+ ->getMock();
$voter
->expects($this->once())
->method('supportsAttribute')
@@ -79,11 +131,46 @@ public function testCacheableVoters()
->method('supportsType')
->with('string')
->willReturn(true);
+
+ $vote = Vote::createGranted();
$voter
->expects($this->once())
- ->method('vote')
+ ->method('getVote')
+ ->with($token, 'bar', ['foo'])
+ ->willReturn($vote);
+
+ $manager = new AccessDecisionManager([$voter]);
+
+ $expectedDecision = AccessDecision::createGranted([$vote]);
+ $this->assertEquals($expectedDecision, $manager->getDecision($token, ['foo'], 'bar'));
+ }
+
+ /**
+ * @group legacy
+ */
+ public function testCacheableVotersLegacy()
+ {
+ $token = $this->createMock(TokenInterface::class);
+ $voter = $this
+ ->getMockBuilder(CacheableVoterInterface::class)
+ ->onlyMethods(['supportsAttribute', 'supportsType', 'vote'])
+ ->addMethods(['getVote'])
+ ->getMock();
+ $voter
+ ->expects($this->once())
+ ->method('supportsAttribute')
+ ->with('foo')
+ ->willReturn(true);
+ $voter
+ ->expects($this->once())
+ ->method('supportsType')
+ ->with('string')
+ ->willReturn(true);
+ $voter
+ ->expects($this->once())
+ ->method('getVote')
->with($token, 'bar', ['foo'])
- ->willReturn(VoterInterface::ACCESS_GRANTED);
+ ->willReturn(Vote::createGranted());
$manager = new AccessDecisionManager([$voter]);
$this->assertTrue($manager->decide($token, ['foo'], 'bar'));
@@ -92,7 +179,11 @@ public function testCacheableVoters()
public function testCacheableVotersIgnoresNonStringAttributes()
{
$token = $this->createMock(TokenInterface::class);
- $voter = $this->getMockBuilder(CacheableVoterInterface::class)->getMockForAbstractClass();
+ $voter = $this
+ ->getMockBuilder(CacheableVoterInterface::class)
+ ->onlyMethods(['supportsAttribute', 'supportsType', 'vote'])
+ ->addMethods(['getVote'])
+ ->getMock();
$voter
->expects($this->never())
->method('supportsAttribute');
@@ -103,18 +194,22 @@ public function testCacheableVotersIgnoresNonStringAttributes()
->willReturn(true);
$voter
->expects($this->once())
- ->method('vote')
+ ->method('getVote')
->with($token, 'bar', [1337])
- ->willReturn(VoterInterface::ACCESS_GRANTED);
+ ->willReturn(Vote::createGranted());
$manager = new AccessDecisionManager([$voter]);
- $this->assertTrue($manager->decide($token, [1337], 'bar'));
+ $this->assertTrue($manager->getDecision($token, [1337], 'bar')->isGranted());
}
public function testCacheableVotersWithMultipleAttributes()
{
$token = $this->createMock(TokenInterface::class);
- $voter = $this->getMockBuilder(CacheableVoterInterface::class)->getMockForAbstractClass();
+ $voter = $this
+ ->getMockBuilder(CacheableVoterInterface::class)
+ ->onlyMethods(['supportsAttribute', 'supportsType', 'vote'])
+ ->addMethods(['getVote'])
+ ->getMock();
$voter
->expects($this->exactly(2))
->method('supportsAttribute')
@@ -136,18 +231,22 @@ public function testCacheableVotersWithMultipleAttributes()
->willReturn(true);
$voter
->expects($this->once())
- ->method('vote')
+ ->method('getVote')
->with($token, 'bar', ['foo', 'bar'])
- ->willReturn(VoterInterface::ACCESS_GRANTED);
+ ->willReturn(Vote::createGranted());
$manager = new AccessDecisionManager([$voter]);
- $this->assertTrue($manager->decide($token, ['foo', 'bar'], 'bar', true));
+ $this->assertTrue($manager->getDecision($token, ['foo', 'bar'], 'bar', true)->isGranted());
}
public function testCacheableVotersWithEmptyAttributes()
{
$token = $this->createMock(TokenInterface::class);
- $voter = $this->getMockBuilder(CacheableVoterInterface::class)->getMockForAbstractClass();
+ $voter = $this
+ ->getMockBuilder(CacheableVoterInterface::class)
+ ->onlyMethods(['supportsAttribute', 'supportsType', 'vote'])
+ ->addMethods(['getVote'])
+ ->getMock();
$voter
->expects($this->never())
->method('supportsAttribute');
@@ -158,18 +257,22 @@ public function testCacheableVotersWithEmptyAttributes()
->willReturn(true);
$voter
->expects($this->once())
- ->method('vote')
+ ->method('getVote')
->with($token, 'bar', [])
- ->willReturn(VoterInterface::ACCESS_GRANTED);
+ ->willReturn(Vote::createGranted());
$manager = new AccessDecisionManager([$voter]);
- $this->assertTrue($manager->decide($token, [], 'bar'));
+ $this->assertTrue($manager->getDecision($token, [], 'bar')->isGranted());
}
public function testCacheableVotersSupportsMethodsCalledOnce()
{
$token = $this->createMock(TokenInterface::class);
- $voter = $this->getMockBuilder(CacheableVoterInterface::class)->getMockForAbstractClass();
+ $voter = $this
+ ->getMockBuilder(CacheableVoterInterface::class)
+ ->onlyMethods(['supportsAttribute', 'supportsType', 'vote'])
+ ->addMethods(['getVote'])
+ ->getMock();
$voter
->expects($this->once())
->method('supportsAttribute')
@@ -182,19 +285,23 @@ public function testCacheableVotersSupportsMethodsCalledOnce()
->willReturn(true);
$voter
->expects($this->exactly(2))
- ->method('vote')
+ ->method('getVote')
->with($token, 'bar', ['foo'])
- ->willReturn(VoterInterface::ACCESS_GRANTED);
+ ->willReturn(Vote::createGranted());
$manager = new AccessDecisionManager([$voter]);
- $this->assertTrue($manager->decide($token, ['foo'], 'bar'));
- $this->assertTrue($manager->decide($token, ['foo'], 'bar'));
+ $this->assertTrue($manager->getDecision($token, ['foo'], 'bar')->isGranted());
+ $this->assertTrue($manager->getDecision($token, ['foo'], 'bar')->isGranted());
}
public function testCacheableVotersNotCalled()
{
$token = $this->createMock(TokenInterface::class);
- $voter = $this->getMockBuilder(CacheableVoterInterface::class)->getMockForAbstractClass();
+ $voter = $this
+ ->getMockBuilder(CacheableVoterInterface::class)
+ ->onlyMethods(['supportsAttribute', 'supportsType', 'vote'])
+ ->addMethods(['getVote'])
+ ->getMock();
$voter
->expects($this->once())
->method('supportsAttribute')
@@ -205,16 +312,20 @@ public function testCacheableVotersNotCalled()
->method('supportsType');
$voter
->expects($this->never())
- ->method('vote');
+ ->method('getVote');
$manager = new AccessDecisionManager([$voter]);
- $this->assertFalse($manager->decide($token, ['foo'], 'bar'));
+ $this->assertFalse($manager->getDecision($token, ['foo'], 'bar')->isGranted());
}
public function testCacheableVotersWithMultipleAttributesAndNonString()
{
$token = $this->createMock(TokenInterface::class);
- $voter = $this->getMockBuilder(CacheableVoterInterface::class)->getMockForAbstractClass();
+ $voter = $this
+ ->getMockBuilder(CacheableVoterInterface::class)
+ ->onlyMethods(['supportsAttribute', 'supportsType', 'vote'])
+ ->addMethods(['getVote'])
+ ->getMock();
$voter
->expects($this->once())
->method('supportsAttribute')
@@ -228,48 +339,29 @@ public function testCacheableVotersWithMultipleAttributesAndNonString()
->willReturn(true);
$voter
->expects($this->once())
- ->method('vote')
+ ->method('getVote')
->with($token, 'bar', ['foo', 1337])
- ->willReturn(VoterInterface::ACCESS_GRANTED);
+ ->willReturn(Vote::createGranted());
$manager = new AccessDecisionManager([$voter]);
- $this->assertTrue($manager->decide($token, ['foo', 1337], 'bar', true));
- }
-
- protected static function getVoters($grants, $denies, $abstains): array
- {
- $voters = [];
- for ($i = 0; $i < $grants; ++$i) {
- $voters[] = self::getVoter(VoterInterface::ACCESS_GRANTED);
- }
- for ($i = 0; $i < $denies; ++$i) {
- $voters[] = self::getVoter(VoterInterface::ACCESS_DENIED);
- }
- for ($i = 0; $i < $abstains; ++$i) {
- $voters[] = self::getVoter(VoterInterface::ACCESS_ABSTAIN);
- }
-
- return $voters;
+ $this->assertTrue($manager->getDecision($token, ['foo', 1337], 'bar', true)->isGranted());
}
- protected static function getVoter($vote)
+ private function getExpectedVoter(Vote $vote): VoterInterface
{
- return new class($vote) implements VoterInterface {
- private int $vote;
-
- public function __construct(int $vote)
- {
- $this->vote = $vote;
- }
+ $voter = $this
+ ->getMockBuilder(VoterInterface::class)
+ ->onlyMethods(['vote'])
+ ->addMethods(['getVote'])
+ ->getMock();
+ $voter->expects($this->once())
+ ->method('getVote')
+ ->willReturn($vote);
- public function vote(TokenInterface $token, $subject, array $attributes)
- {
- return $this->vote;
- }
- };
+ return $voter;
}
- private function getExpectedVoter(int $vote): VoterInterface
+ private function getExpectedVoterLegacy(int $vote): VoterInterface
{
$voter = $this->createMock(VoterInterface::class);
$voter->expects($this->once())
diff --git a/src/Symfony/Component/Security/Core/Tests/Authorization/AuthorizationCheckerTest.php b/src/Symfony/Component/Security/Core/Tests/Authorization/AuthorizationCheckerTest.php
index 36b048c8976d1..c604be2877551 100644
--- a/src/Symfony/Component/Security/Core/Tests/Authorization/AuthorizationCheckerTest.php
+++ b/src/Symfony/Component/Security/Core/Tests/Authorization/AuthorizationCheckerTest.php
@@ -13,15 +13,19 @@
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
+use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait;
use Symfony\Component\Security\Core\Authentication\Token\NullToken;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
+use Symfony\Component\Security\Core\Authorization\AccessDecision;
use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface;
use Symfony\Component\Security\Core\Authorization\AuthorizationChecker;
use Symfony\Component\Security\Core\User\InMemoryUser;
class AuthorizationCheckerTest extends TestCase
{
+ use ExpectDeprecationTrait;
+
private MockObject&AccessDecisionManagerInterface $accessDecisionManager;
private AuthorizationChecker $authorizationChecker;
private TokenStorage $tokenStorage;
@@ -35,26 +39,84 @@ protected function setUp(): void
}
public function testVoteWithoutAuthenticationToken()
+ {
+ $accessDecisionManager = $this
+ ->getMockBuilder(AccessDecisionManagerInterface::class)
+ ->onlyMethods(['decide'])
+ ->addMethods(['getDecision'])
+ ->getMock();
+ $accessDecisionManager
+ ->expects($this->once())
+ ->method('getDecision')
+ ->with($this->isInstanceOf(NullToken::class), ['ROLE_FOO'])
+ ->willReturn(AccessDecision::createGranted());
+
+ $authorizationChecker = new AuthorizationChecker(
+ $this->tokenStorage,
+ $accessDecisionManager
+ );
+
+ $authorizationChecker->isGranted('ROLE_FOO');
+ }
+
+ /**
+ * @group legacy
+ */
+ public function testVoteWithoutAuthenticationTokenLegacy()
{
$authorizationChecker = new AuthorizationChecker($this->tokenStorage, $this->accessDecisionManager);
$this->accessDecisionManager->expects($this->once())->method('decide')->with($this->isInstanceOf(NullToken::class))->willReturn(false);
+ $this->expectDeprecation('Since symfony/security-core 7.1: Not implementing "%s::getDecision()" method is deprecated, and would be required in 7.0.');
$authorizationChecker->isGranted('ROLE_FOO');
}
/**
+ * @group legacy
+ *
* @dataProvider isGrantedProvider
*/
public function testIsGranted($decide)
{
$token = new UsernamePasswordToken(new InMemoryUser('username', 'password', ['ROLE_USER']), 'provider', ['ROLE_USER']);
+ $accessDecisionManager = $this
+ ->getMockBuilder(AccessDecisionManagerInterface::class)
+ ->onlyMethods(['decide'])
+ ->addMethods(['getDecision'])
+ ->getMock();
+ $accessDecisionManager
+ ->expects($this->once())
+ ->method('getDecision')
+ ->with($this->identicalTo($token), $this->identicalTo(['ROLE_FOO']))
+ ->willReturn($decide ? AccessDecision::createGranted() : AccessDecision::createDenied());
+
+ $this->tokenStorage->setToken($token);
+ $authorizationChecker = new AuthorizationChecker(
+ $this->tokenStorage,
+ $accessDecisionManager
+ );
+
+ $this->assertSame($decide, $authorizationChecker->isGranted('ROLE_FOO'));
+ }
+
+ /**
+ * @group legacy
+ *
+ * @dataProvider isGrantedProvider
+ */
+ public function testIsGrantedLegacy($decide)
+ {
+ $token = new UsernamePasswordToken(new InMemoryUser('username', 'password', ['ROLE_USER']), 'provider', ['ROLE_USER']);
+
$this->accessDecisionManager
->expects($this->once())
->method('decide')
->willReturn($decide);
$this->tokenStorage->setToken($token);
+
+ $this->expectDeprecation('Since symfony/security-core 7.1: Not implementing "%s::getDecision()" method is deprecated, and would be required in 7.0.');
$this->assertSame($decide, $this->authorizationChecker->isGranted('ROLE_FOO'));
}
@@ -69,12 +131,43 @@ public function testIsGrantedWithObjectAttribute()
$token = new UsernamePasswordToken(new InMemoryUser('username', 'password', ['ROLE_USER']), 'provider', ['ROLE_USER']);
+ $accessDecisionManager = $this
+ ->getMockBuilder(AccessDecisionManagerInterface::class)
+ ->onlyMethods(['decide'])
+ ->addMethods(['getDecision'])
+ ->getMock();
+ $accessDecisionManager
+ ->expects($this->once())
+ ->method('getDecision')
+ ->with($this->identicalTo($token), $this->identicalTo([$attribute]))
+ ->willReturn(AccessDecision::createGranted());
+
+ $this->tokenStorage->setToken($token);
+ $authorizationChecker = new AuthorizationChecker(
+ $this->tokenStorage,
+ $accessDecisionManager
+ );
+
+ $this->assertTrue($authorizationChecker->isGranted($attribute));
+ }
+
+ /**
+ * @group legacy
+ */
+ public function testIsGrantedWithObjectAttributeLegacy()
+ {
+ $attribute = new \stdClass();
+
+ $token = new UsernamePasswordToken(new InMemoryUser('username', 'password', ['ROLE_USER']), 'provider', ['ROLE_USER']);
+
$this->accessDecisionManager
->expects($this->once())
->method('decide')
->with($this->identicalTo($token), $this->identicalTo([$attribute]))
->willReturn(true);
$this->tokenStorage->setToken($token);
+
+ $this->expectDeprecation('Since symfony/security-core 7.1: Not implementing "%s::getDecision()" method is deprecated, and would be required in 7.0.');
$this->assertTrue($this->authorizationChecker->isGranted($attribute));
}
}
diff --git a/src/Symfony/Component/Security/Core/Tests/Authorization/Strategy/AffirmativeStrategyTest.php b/src/Symfony/Component/Security/Core/Tests/Authorization/Strategy/AffirmativeStrategyTest.php
index b467a920b0f67..3f89f1e6dd5bd 100644
--- a/src/Symfony/Component/Security/Core/Tests/Authorization/Strategy/AffirmativeStrategyTest.php
+++ b/src/Symfony/Component/Security/Core/Tests/Authorization/Strategy/AffirmativeStrategyTest.php
@@ -9,9 +9,11 @@
* file that was distributed with this source code.
*/
-namespace Authorization\Strategy;
+namespace Symfony\Component\Security\Core\Tests\Authorization\Strategy;
+use Symfony\Component\Security\Core\Authorization\AccessDecision;
use Symfony\Component\Security\Core\Authorization\Strategy\AffirmativeStrategy;
+use Symfony\Component\Security\Core\Authorization\Voter\Vote;
use Symfony\Component\Security\Core\Test\AccessDecisionStrategyTestCase;
class AffirmativeStrategyTest extends AccessDecisionStrategyTestCase
@@ -20,13 +22,23 @@ public static function provideStrategyTests(): iterable
{
$strategy = new AffirmativeStrategy();
- yield [$strategy, self::getVoters(1, 0, 0), true];
- yield [$strategy, self::getVoters(1, 2, 0), true];
- yield [$strategy, self::getVoters(0, 1, 0), false];
- yield [$strategy, self::getVoters(0, 0, 1), false];
+ yield [$strategy, self::getVoters(1, 0, 0), AccessDecision::createGranted([
+ Vote::createGranted(),
+ ])];
+ yield [$strategy, self::getVoters(1, 2, 0), AccessDecision::createGranted([
+ Vote::createGranted(),
+ ])];
+ yield [$strategy, self::getVoters(0, 1, 0), AccessDecision::createDenied([
+ Vote::createDenied(),
+ ])];
+ yield [$strategy, self::getVoters(0, 0, 1), AccessDecision::createDenied([
+ Vote::createAbstain(),
+ ])];
$strategy = new AffirmativeStrategy(true);
- yield [$strategy, self::getVoters(0, 0, 1), true];
+ yield [$strategy, self::getVoters(0, 0, 1), AccessDecision::createGranted([
+ Vote::createAbstain(),
+ ])];
}
}
diff --git a/src/Symfony/Component/Security/Core/Tests/Authorization/Strategy/ConsensusStrategyTest.php b/src/Symfony/Component/Security/Core/Tests/Authorization/Strategy/ConsensusStrategyTest.php
index bde6fb0d624b7..47eac3e115aa0 100644
--- a/src/Symfony/Component/Security/Core/Tests/Authorization/Strategy/ConsensusStrategyTest.php
+++ b/src/Symfony/Component/Security/Core/Tests/Authorization/Strategy/ConsensusStrategyTest.php
@@ -9,9 +9,11 @@
* file that was distributed with this source code.
*/
-namespace Authorization\Strategy;
+namespace Symfony\Component\Security\Core\Tests\Authorization\Strategy;
+use Symfony\Component\Security\Core\Authorization\AccessDecision;
use Symfony\Component\Security\Core\Authorization\Strategy\ConsensusStrategy;
+use Symfony\Component\Security\Core\Authorization\Voter\Vote;
use Symfony\Component\Security\Core\Test\AccessDecisionStrategyTestCase;
class ConsensusStrategyTest extends AccessDecisionStrategyTestCase
@@ -20,21 +22,57 @@ public static function provideStrategyTests(): iterable
{
$strategy = new ConsensusStrategy();
- yield [$strategy, self::getVoters(1, 0, 0), true];
- yield [$strategy, self::getVoters(1, 2, 0), false];
- yield [$strategy, self::getVoters(2, 1, 0), true];
- yield [$strategy, self::getVoters(0, 0, 1), false];
+ yield [$strategy, self::getVoters(1, 0, 0), AccessDecision::createGranted([
+ Vote::createGranted(),
+ ])];
+ yield [$strategy, self::getVoters(1, 2, 0), AccessDecision::createDenied([
+ Vote::createGranted(),
+ Vote::createDenied(),
+ Vote::createDenied(),
+ ])];
+ yield [$strategy, self::getVoters(2, 1, 0), AccessDecision::createGranted([
+ Vote::createGranted(),
+ Vote::createGranted(),
+ Vote::createDenied(),
+ ])];
+ yield [$strategy, self::getVoters(0, 0, 1), AccessDecision::createDenied([
+ Vote::createAbstain(),
+ ])];
- yield [$strategy, self::getVoters(2, 2, 0), true];
- yield [$strategy, self::getVoters(2, 2, 1), true];
+ yield [$strategy, self::getVoters(2, 2, 0), AccessDecision::createGranted([
+ Vote::createGranted(),
+ Vote::createGranted(),
+ Vote::createDenied(),
+ Vote::createDenied(),
+ ])];
+ yield [$strategy, self::getVoters(2, 2, 1), AccessDecision::createGranted([
+ Vote::createGranted(),
+ Vote::createGranted(),
+ Vote::createDenied(),
+ Vote::createDenied(),
+ Vote::createAbstain(),
+ ])];
$strategy = new ConsensusStrategy(true);
- yield [$strategy, self::getVoters(0, 0, 1), true];
+ yield [$strategy, self::getVoters(0, 0, 1), AccessDecision::createGranted([
+ Vote::createAbstain(),
+ ])];
$strategy = new ConsensusStrategy(false, false);
- yield [$strategy, self::getVoters(2, 2, 0), false];
- yield [$strategy, self::getVoters(2, 2, 1), false];
+ yield [$strategy, self::getVoters(2, 2, 0), AccessDecision::createDenied([
+ Vote::createGranted(),
+ Vote::createGranted(),
+ Vote::createDenied(),
+ Vote::createDenied(),
+ ])];
+ yield [$strategy, self::getVoters(2, 2, 1), AccessDecision::createDenied([
+ Vote::createGranted(),
+ Vote::createGranted(),
+ Vote::createDenied(),
+ Vote::createDenied(),
+ Vote::createAbstain(),
+ ])];
}
}
diff --git a/src/Symfony/Component/Security/Core/Tests/Authorization/Strategy/PriorityStrategyTest.php b/src/Symfony/Component/Security/Core/Tests/Authorization/Strategy/PriorityStrategyTest.php
index aef3aaf0b27e3..807fb7b0f72bd 100644
--- a/src/Symfony/Component/Security/Core/Tests/Authorization/Strategy/PriorityStrategyTest.php
+++ b/src/Symfony/Component/Security/Core/Tests/Authorization/Strategy/PriorityStrategyTest.php
@@ -9,9 +9,11 @@
* file that was distributed with this source code.
*/
-namespace Authorization\Strategy;
+namespace Symfony\Component\Security\Core\Tests\Authorization\Strategy;
+use Symfony\Component\Security\Core\Authorization\AccessDecision;
use Symfony\Component\Security\Core\Authorization\Strategy\PriorityStrategy;
+use Symfony\Component\Security\Core\Authorization\Voter\Vote;
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
use Symfony\Component\Security\Core\Test\AccessDecisionStrategyTestCase;
@@ -26,19 +28,31 @@ public static function provideStrategyTests(): iterable
self::getVoter(VoterInterface::ACCESS_GRANTED),
self::getVoter(VoterInterface::ACCESS_DENIED),
self::getVoter(VoterInterface::ACCESS_DENIED),
- ], true];
+ ], AccessDecision::createGranted([
+ Vote::createAbstain(),
+ Vote::createGranted(),
+ ])];
yield [$strategy, [
self::getVoter(VoterInterface::ACCESS_ABSTAIN),
self::getVoter(VoterInterface::ACCESS_DENIED),
self::getVoter(VoterInterface::ACCESS_GRANTED),
self::getVoter(VoterInterface::ACCESS_GRANTED),
- ], false];
+ ], AccessDecision::createDenied([
+ Vote::createAbstain(),
+ Vote::createDenied(),
+ ])];
- yield [$strategy, self::getVoters(0, 0, 2), false];
+ yield [$strategy, self::getVoters(0, 0, 2), AccessDecision::createDenied([
+ Vote::createAbstain(),
+ Vote::createAbstain(),
+ ])];
$strategy = new PriorityStrategy(true);
- yield [$strategy, self::getVoters(0, 0, 2), true];
+ yield [$strategy, self::getVoters(0, 0, 2), AccessDecision::createGranted([
+ Vote::createAbstain(),
+ Vote::createAbstain(),
+ ])];
}
}
diff --git a/src/Symfony/Component/Security/Core/Tests/Authorization/Strategy/UnanimousStrategyTest.php b/src/Symfony/Component/Security/Core/Tests/Authorization/Strategy/UnanimousStrategyTest.php
index e00a50e3186ba..f51d142ab98cf 100644
--- a/src/Symfony/Component/Security/Core/Tests/Authorization/Strategy/UnanimousStrategyTest.php
+++ b/src/Symfony/Component/Security/Core/Tests/Authorization/Strategy/UnanimousStrategyTest.php
@@ -9,9 +9,11 @@
* file that was distributed with this source code.
*/
-namespace Authorization\Strategy;
+namespace Symfony\Component\Security\Core\Tests\Authorization\Strategy;
+use Symfony\Component\Security\Core\Authorization\AccessDecision;
use Symfony\Component\Security\Core\Authorization\Strategy\UnanimousStrategy;
+use Symfony\Component\Security\Core\Authorization\Voter\Vote;
use Symfony\Component\Security\Core\Test\AccessDecisionStrategyTestCase;
class UnanimousStrategyTest extends AccessDecisionStrategyTestCase
@@ -20,14 +22,28 @@ public static function provideStrategyTests(): iterable
{
$strategy = new UnanimousStrategy();
- yield [$strategy, self::getVoters(1, 0, 0), true];
- yield [$strategy, self::getVoters(1, 0, 1), true];
- yield [$strategy, self::getVoters(1, 1, 0), false];
-
- yield [$strategy, self::getVoters(0, 0, 2), false];
+ yield [$strategy, self::getVoters(1, 0, 0), AccessDecision::createGranted([
+ Vote::createGranted(),
+ ])];
+ yield [$strategy, self::getVoters(1, 0, 1), AccessDecision::createGranted([
+ Vote::createGranted(),
+ Vote::createAbstain(),
+ ])];
+ yield [$strategy, self::getVoters(1, 1, 0), AccessDecision::createDenied([
+ Vote::createGranted(),
+ Vote::createDenied(),
+ ])];
+
+ yield [$strategy, self::getVoters(0, 0, 2), AccessDecision::createDenied([
+ Vote::createAbstain(),
+ Vote::createAbstain(),
+ ])];
$strategy = new UnanimousStrategy(true);
- yield [$strategy, self::getVoters(0, 0, 2), true];
+ yield [$strategy, self::getVoters(0, 0, 2), AccessDecision::createGranted([
+ Vote::createAbstain(),
+ Vote::createAbstain(),
+ ])];
}
}
diff --git a/src/Symfony/Component/Security/Core/Tests/Authorization/TraceableAccessDecisionManagerTest.php b/src/Symfony/Component/Security/Core/Tests/Authorization/TraceableAccessDecisionManagerTest.php
index c14ee1fa459d0..bc1c32be3c141 100644
--- a/src/Symfony/Component/Security/Core/Tests/Authorization/TraceableAccessDecisionManagerTest.php
+++ b/src/Symfony/Component/Security/Core/Tests/Authorization/TraceableAccessDecisionManagerTest.php
@@ -13,9 +13,11 @@
use PHPUnit\Framework\TestCase;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
+use Symfony\Component\Security\Core\Authorization\AccessDecision;
use Symfony\Component\Security\Core\Authorization\AccessDecisionManager;
use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface;
use Symfony\Component\Security\Core\Authorization\TraceableAccessDecisionManager;
+use Symfony\Component\Security\Core\Authorization\Voter\Vote;
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
use Symfony\Component\Security\Core\Tests\Fixtures\DummyVoter;
@@ -24,28 +26,32 @@ class TraceableAccessDecisionManagerTest extends TestCase
/**
* @dataProvider provideObjectsAndLogs
*/
- public function testDecideLog(array $expectedLog, array $attributes, $object, array $voterVotes, bool $result)
+ public function testDecideLog(array $expectedLog, array $attributes, $object, array $voterVotes, AccessDecision $decision)
{
$token = $this->createMock(TokenInterface::class);
- $admMock = $this->createMock(AccessDecisionManagerInterface::class);
+ $admMock = $this
+ ->getMockBuilder(AccessDecisionManagerInterface::class)
+ ->onlyMethods(['decide'])
+ ->addMethods(['getDecision'])
+ ->getMock();
$adm = new TraceableAccessDecisionManager($admMock);
$admMock
->expects($this->once())
- ->method('decide')
+ ->method('getDecision')
->with($token, $attributes, $object)
- ->willReturnCallback(function ($token, $attributes, $object) use ($voterVotes, $adm, $result) {
+ ->willReturnCallback(function ($token, $attributes, $object) use ($voterVotes, $adm, $decision) {
foreach ($voterVotes as $voterVote) {
[$voter, $vote] = $voterVote;
$adm->addVoterVote($voter, $attributes, $vote);
}
- return $result;
+ return $decision;
})
;
- $adm->decide($token, $attributes, $object);
+ $adm->getDecision($token, $attributes, $object);
$this->assertEquals($expectedLog, $adm->getDecisionLog());
}
@@ -59,115 +65,115 @@ public static function provideObjectsAndLogs(): \Generator
[[
'attributes' => ['ATTRIBUTE_1'],
'object' => null,
- 'result' => true,
+ 'result' => ($result = AccessDecision::createGranted()),
'voterDetails' => [
- ['voter' => $voter1, 'attributes' => ['ATTRIBUTE_1'], 'vote' => VoterInterface::ACCESS_GRANTED],
- ['voter' => $voter2, 'attributes' => ['ATTRIBUTE_1'], 'vote' => VoterInterface::ACCESS_GRANTED],
+ ['voter' => $voter1, 'attributes' => ['ATTRIBUTE_1'], 'vote' => $granted_vote1 = Vote::createGranted()],
+ ['voter' => $voter2, 'attributes' => ['ATTRIBUTE_1'], 'vote' => $granted_vote2 = Vote::createGranted()],
],
]],
['ATTRIBUTE_1'],
null,
[
- [$voter1, VoterInterface::ACCESS_GRANTED],
- [$voter2, VoterInterface::ACCESS_GRANTED],
+ [$voter1, $granted_vote1],
+ [$voter2, $granted_vote2],
],
- true,
+ $result,
];
yield [
[[
'attributes' => ['ATTRIBUTE_1', 'ATTRIBUTE_2'],
'object' => true,
- 'result' => false,
+ 'result' => ($result = AccessDecision::createDenied()),
'voterDetails' => [
- ['voter' => $voter1, 'attributes' => ['ATTRIBUTE_1', 'ATTRIBUTE_2'], 'vote' => VoterInterface::ACCESS_ABSTAIN],
- ['voter' => $voter2, 'attributes' => ['ATTRIBUTE_1', 'ATTRIBUTE_2'], 'vote' => VoterInterface::ACCESS_GRANTED],
+ ['voter' => $voter1, 'attributes' => ['ATTRIBUTE_1', 'ATTRIBUTE_2'], 'vote' => $abstain_vote1 = Vote::createAbstain()],
+ ['voter' => $voter2, 'attributes' => ['ATTRIBUTE_1', 'ATTRIBUTE_2'], 'vote' => $granted_vote2 = Vote::createGranted()],
],
]],
['ATTRIBUTE_1', 'ATTRIBUTE_2'],
true,
[
- [$voter1, VoterInterface::ACCESS_ABSTAIN],
- [$voter2, VoterInterface::ACCESS_GRANTED],
+ [$voter1, $abstain_vote1],
+ [$voter2, $granted_vote2],
],
- false,
+ $result,
];
yield [
[[
'attributes' => [null],
'object' => 'jolie string',
- 'result' => false,
+ 'result' => ($result = AccessDecision::createDenied()),
'voterDetails' => [
- ['voter' => $voter1, 'attributes' => [null], 'vote' => VoterInterface::ACCESS_ABSTAIN],
- ['voter' => $voter2, 'attributes' => [null], 'vote' => VoterInterface::ACCESS_DENIED],
+ ['voter' => $voter1, 'attributes' => [null], 'vote' => $abstain_vote1 = Vote::createAbstain()],
+ ['voter' => $voter2, 'attributes' => [null], 'vote' => $abstain_denied2 = Vote::createAbstain()],
],
]],
[null],
'jolie string',
[
- [$voter1, VoterInterface::ACCESS_ABSTAIN],
- [$voter2, VoterInterface::ACCESS_DENIED],
+ [$voter1, $abstain_vote1],
+ [$voter2, $abstain_denied2],
],
- false,
+ $result,
];
yield [
[[
'attributes' => [12],
'object' => 12345,
- 'result' => true,
+ 'result' => ($result = AccessDecision::createGranted()),
'voterDetails' => [],
]],
'attributes' => [12],
12345,
[],
- true,
+ $result,
];
yield [
[[
'attributes' => [new \stdClass()],
'object' => $x = fopen(__FILE__, 'r'),
- 'result' => true,
+ 'result' => ($result = AccessDecision::createGranted()),
'voterDetails' => [],
]],
[new \stdClass()],
$x,
[],
- true,
+ $result,
];
yield [
[[
'attributes' => ['ATTRIBUTE_2'],
'object' => $x = [],
- 'result' => false,
+ 'result' => ($result = AccessDecision::createDenied()),
'voterDetails' => [
- ['voter' => $voter1, 'attributes' => ['ATTRIBUTE_2'], 'vote' => VoterInterface::ACCESS_ABSTAIN],
- ['voter' => $voter2, 'attributes' => ['ATTRIBUTE_2'], 'vote' => VoterInterface::ACCESS_ABSTAIN],
+ ['voter' => $voter1, 'attributes' => ['ATTRIBUTE_2'], 'vote' => $abstain_vote1 = Vote::createAbstain()],
+ ['voter' => $voter2, 'attributes' => ['ATTRIBUTE_2'], 'vote' => $abstain_vote2 = Vote::createAbstain()],
],
]],
['ATTRIBUTE_2'],
$x,
[
- [$voter1, VoterInterface::ACCESS_ABSTAIN],
- [$voter2, VoterInterface::ACCESS_ABSTAIN],
+ [$voter1, $abstain_vote1],
+ [$voter2, $abstain_vote2],
],
- false,
+ $result,
];
yield [
[[
'attributes' => [12.13],
'object' => new \stdClass(),
- 'result' => false,
+ 'result' => ($result = AccessDecision::createDenied()),
'voterDetails' => [
- ['voter' => $voter1, 'attributes' => [12.13], 'vote' => VoterInterface::ACCESS_DENIED],
- ['voter' => $voter2, 'attributes' => [12.13], 'vote' => VoterInterface::ACCESS_DENIED],
+ ['voter' => $voter1, 'attributes' => [12.13], 'vote' => $denied_vote1 = Vote::createDenied()],
+ ['voter' => $voter2, 'attributes' => [12.13], 'vote' => $denied_vote2 = Vote::createDenied()],
],
]],
[12.13],
new \stdClass(),
[
- [$voter1, VoterInterface::ACCESS_DENIED],
- [$voter2, VoterInterface::ACCESS_DENIED],
+ [$voter1, $denied_vote1],
+ [$voter2, $denied_vote2],
],
- false,
+ $result,
];
}
@@ -179,25 +185,28 @@ public function testAccessDecisionManagerCalledByVoter()
$voter1 = $this
->getMockBuilder(VoterInterface::class)
->onlyMethods(['vote'])
+ ->addMethods(['getVote'])
->getMock();
$voter2 = $this
->getMockBuilder(VoterInterface::class)
->onlyMethods(['vote'])
+ ->addMethods(['getVote'])
->getMock();
$voter3 = $this
->getMockBuilder(VoterInterface::class)
->onlyMethods(['vote'])
+ ->addMethods(['getVote'])
->getMock();
$sut = new TraceableAccessDecisionManager(new AccessDecisionManager([$voter1, $voter2, $voter3]));
$voter1
->expects($this->any())
- ->method('vote')
+ ->method('getVote')
->willReturnCallback(function (TokenInterface $token, $subject, array $attributes) use ($sut, $voter1) {
- $vote = \in_array('attr1', $attributes, true) ? VoterInterface::ACCESS_GRANTED : VoterInterface::ACCESS_ABSTAIN;
+ $vote = \in_array('attr1', $attributes, true) ? Vote::createGranted() : Vote::createAbstain();
$sut->addVoterVote($voter1, $attributes, $vote);
return $vote;
@@ -205,12 +214,12 @@ public function testAccessDecisionManagerCalledByVoter()
$voter2
->expects($this->any())
- ->method('vote')
+ ->method('getVote')
->willReturnCallback(function (TokenInterface $token, $subject, array $attributes) use ($sut, $voter2) {
if (\in_array('attr2', $attributes, true)) {
- $vote = null == $subject ? VoterInterface::ACCESS_GRANTED : VoterInterface::ACCESS_DENIED;
+ $vote = null == $subject ? Vote::createGranted() : Vote::createDenied();
} else {
- $vote = VoterInterface::ACCESS_ABSTAIN;
+ $vote = Vote::createAbstain();
}
$sut->addVoterVote($voter2, $attributes, $vote);
@@ -220,12 +229,12 @@ public function testAccessDecisionManagerCalledByVoter()
$voter3
->expects($this->any())
- ->method('vote')
+ ->method('getVote')
->willReturnCallback(function (TokenInterface $token, $subject, array $attributes) use ($sut, $voter3) {
if (\in_array('attr2', $attributes, true) && $subject) {
- $vote = $sut->decide($token, $attributes) ? VoterInterface::ACCESS_GRANTED : VoterInterface::ACCESS_DENIED;
+ $vote = $sut->getDecision($token, $attributes)->isGranted() ? Vote::createGranted() : Vote::createDenied();
} else {
- $vote = VoterInterface::ACCESS_ABSTAIN;
+ $vote = Vote::createAbstain();
}
$sut->addVoterVote($voter3, $attributes, $vote);
@@ -234,36 +243,45 @@ public function testAccessDecisionManagerCalledByVoter()
});
$token = $this->createMock(TokenInterface::class);
- $sut->decide($token, ['attr1'], null);
- $sut->decide($token, ['attr2'], $obj = new \stdClass());
+ $sut->getDecision($token, ['attr1'], null);
+ $sut->getDecision($token, ['attr2'], $obj = new \stdClass());
$this->assertEquals([
[
'attributes' => ['attr1'],
'object' => null,
'voterDetails' => [
- ['voter' => $voter1, 'attributes' => ['attr1'], 'vote' => VoterInterface::ACCESS_GRANTED],
+ ['voter' => $voter1, 'attributes' => ['attr1'], 'vote' => Vote::createGranted()],
],
- 'result' => true,
+ 'result' => AccessDecision::createGranted([
+ Vote::createGranted(),
+ ]),
],
[
'attributes' => ['attr2'],
'object' => null,
'voterDetails' => [
- ['voter' => $voter1, 'attributes' => ['attr2'], 'vote' => VoterInterface::ACCESS_ABSTAIN],
- ['voter' => $voter2, 'attributes' => ['attr2'], 'vote' => VoterInterface::ACCESS_GRANTED],
+ ['voter' => $voter1, 'attributes' => ['attr2'], 'vote' => Vote::createAbstain()],
+ ['voter' => $voter2, 'attributes' => ['attr2'], 'vote' => Vote::createGranted()],
],
- 'result' => true,
+ 'result' => AccessDecision::createGranted([
+ Vote::createAbstain(),
+ Vote::createGranted(),
+ ]),
],
[
'attributes' => ['attr2'],
'object' => $obj,
'voterDetails' => [
- ['voter' => $voter1, 'attributes' => ['attr2'], 'vote' => VoterInterface::ACCESS_ABSTAIN],
- ['voter' => $voter2, 'attributes' => ['attr2'], 'vote' => VoterInterface::ACCESS_DENIED],
- ['voter' => $voter3, 'attributes' => ['attr2'], 'vote' => VoterInterface::ACCESS_GRANTED],
+ ['voter' => $voter1, 'attributes' => ['attr2'], 'vote' => Vote::createAbstain()],
+ ['voter' => $voter2, 'attributes' => ['attr2'], 'vote' => Vote::createDenied()],
+ ['voter' => $voter3, 'attributes' => ['attr2'], 'vote' => Vote::createGranted()],
],
- 'result' => true,
+ 'result' => AccessDecision::createGranted([
+ Vote::createAbstain(),
+ Vote::createDenied(),
+ Vote::createGranted(),
+ ]),
],
], $sut->getDecisionLog());
}
diff --git a/src/Symfony/Component/Security/Core/Tests/Authorization/Voter/AuthenticatedVoterTest.php b/src/Symfony/Component/Security/Core/Tests/Authorization/Voter/AuthenticatedVoterTest.php
index 88544c081f78c..b9a840fb6ce71 100644
--- a/src/Symfony/Component/Security/Core/Tests/Authorization/Voter/AuthenticatedVoterTest.php
+++ b/src/Symfony/Component/Security/Core/Tests/Authorization/Voter/AuthenticatedVoterTest.php
@@ -12,28 +12,65 @@
namespace Symfony\Component\Security\Core\Tests\Authorization\Voter;
use PHPUnit\Framework\TestCase;
+use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait;
use Symfony\Component\Security\Core\Authentication\AuthenticationTrustResolver;
use Symfony\Component\Security\Core\Authentication\Token\AbstractToken;
use Symfony\Component\Security\Core\Authentication\Token\NullToken;
use Symfony\Component\Security\Core\Authentication\Token\RememberMeToken;
use Symfony\Component\Security\Core\Authentication\Token\SwitchUserToken;
use Symfony\Component\Security\Core\Authorization\Voter\AuthenticatedVoter;
+use Symfony\Component\Security\Core\Authorization\Voter\Vote;
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
use Symfony\Component\Security\Core\User\InMemoryUser;
class AuthenticatedVoterTest extends TestCase
{
+ use ExpectDeprecationTrait;
+
+ /**
+ * @dataProvider getGetVoteTests
+ */
+ public function testGetVote($authenticated, $attributes, $expected)
+ {
+ $voter = new AuthenticatedVoter(new AuthenticationTrustResolver());
+
+ $this->assertEquals($expected, $voter->getVote($this->getToken($authenticated), null, $attributes));
+ }
+
+ public static function getGetVoteTests()
+ {
+ return [
+ ['fully', [], Vote::createAbstain()],
+ ['fully', ['FOO'], Vote::createAbstain()],
+ ['remembered', [], Vote::createAbstain()],
+ ['remembered', ['FOO'], Vote::createAbstain()],
+
+ ['fully', ['IS_AUTHENTICATED_REMEMBERED'], Vote::createGranted()],
+ ['remembered', ['IS_AUTHENTICATED_REMEMBERED'], Vote::createGranted()],
+
+ ['fully', ['IS_AUTHENTICATED_FULLY'], Vote::createGranted()],
+ ['remembered', ['IS_AUTHENTICATED_FULLY'], Vote::createDenied()],
+
+ ['fully', ['IS_IMPERSONATOR'], Vote::createDenied()],
+ ['remembered', ['IS_IMPERSONATOR'], Vote::createDenied()],
+ ['impersonated', ['IS_IMPERSONATOR'], Vote::createGranted()],
+ ];
+ }
+
/**
- * @dataProvider getVoteTests
+ * @group legacy
+ *
+ * @dataProvider getVoteTestsLegacy
*/
- public function testVote($authenticated, $attributes, $expected)
+ public function testVoteLegacy($authenticated, $attributes, $expected)
{
$voter = new AuthenticatedVoter(new AuthenticationTrustResolver());
+ $this->expectDeprecation('Since symfony/security-core 7.1: Method "%s::vote()" has been deprecated, use "%s::getVote()" instead.');
$this->assertSame($expected, $voter->vote($this->getToken($authenticated), null, $attributes));
}
- public static function getVoteTests()
+ public static function getVoteTestsLegacy()
{
return [
['fully', [], VoterInterface::ACCESS_ABSTAIN],
diff --git a/src/Symfony/Component/Security/Core/Tests/Authorization/Voter/ExpressionVoterTest.php b/src/Symfony/Component/Security/Core/Tests/Authorization/Voter/ExpressionVoterTest.php
index 369b17f0460ea..aaa7dfb4ca364 100644
--- a/src/Symfony/Component/Security/Core/Tests/Authorization/Voter/ExpressionVoterTest.php
+++ b/src/Symfony/Component/Security/Core/Tests/Authorization/Voter/ExpressionVoterTest.php
@@ -12,27 +12,57 @@
namespace Symfony\Component\Security\Core\Tests\Authorization\Voter;
use PHPUnit\Framework\TestCase;
+use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait;
use Symfony\Component\ExpressionLanguage\Expression;
use Symfony\Component\Security\Core\Authentication\AuthenticationTrustResolverInterface;
use Symfony\Component\Security\Core\Authentication\Token\AbstractToken;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
use Symfony\Component\Security\Core\Authorization\ExpressionLanguage;
use Symfony\Component\Security\Core\Authorization\Voter\ExpressionVoter;
+use Symfony\Component\Security\Core\Authorization\Voter\Vote;
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
class ExpressionVoterTest extends TestCase
{
+ use ExpectDeprecationTrait;
+
+ /**
+ * @dataProvider getGetVoteTests
+ */
+ public function testGetVoteWithTokenThatReturnsRoleNames(array $roles, array $attributes, Vote $expected, $tokenExpectsGetRoles = true, $expressionLanguageExpectsEvaluate = true)
+ {
+ $voter = new ExpressionVoter($this->createExpressionLanguage($expressionLanguageExpectsEvaluate), $this->createTrustResolver(), $this->createAuthorizationChecker());
+
+ $this->assertEquals($expected, $voter->getVote($this->getTokenWithRoleNames($roles, $tokenExpectsGetRoles), null, $attributes));
+ }
+
+ public function getGetVoteTests()
+ {
+ return [
+ [[], [], Vote::createAbstain(), false, false],
+ [[], ['FOO'], Vote::createAbstain(), false, false],
+
+ [[], [$this->createExpression()], Vote::createDenied(), true, false],
+
+ [['ROLE_FOO'], [$this->createExpression(), $this->createExpression()], Vote::createGranted()],
+ [['ROLE_BAR', 'ROLE_FOO'], [$this->createExpression()], Vote::createGranted()],
+ ];
+ }
+
/**
- * @dataProvider getVoteTests
+ * @group legacy
+ *
+ * @dataProvider getVoteTestsLegacy
*/
- public function testVoteWithTokenThatReturnsRoleNames($roles, $attributes, $expected, $tokenExpectsGetRoles = true, $expressionLanguageExpectsEvaluate = true)
+ public function testVoteWithTokenThatReturnsRoleNamesLegacy($roles, $attributes, $expected, $tokenExpectsGetRoles = true, $expressionLanguageExpectsEvaluate = true)
{
$voter = new ExpressionVoter($this->createExpressionLanguage($expressionLanguageExpectsEvaluate), $this->createTrustResolver(), $this->createAuthorizationChecker());
+ $this->expectDeprecation('Since symfony/security-core 7.1: Method "%s::vote()" has been deprecated, use "%s::getVote()" instead.');
$this->assertSame($expected, $voter->vote($this->getTokenWithRoleNames($roles, $tokenExpectsGetRoles), null, $attributes));
}
- public static function getVoteTests()
+ public static function getVoteTestsLegacy()
{
return [
[[], [], VoterInterface::ACCESS_ABSTAIN, false, false],
diff --git a/src/Symfony/Component/Security/Core/Tests/Authorization/Voter/RoleHierarchyVoterTest.php b/src/Symfony/Component/Security/Core/Tests/Authorization/Voter/RoleHierarchyVoterTest.php
index b811bd745bb85..ffdf14352be2d 100644
--- a/src/Symfony/Component/Security/Core/Tests/Authorization/Voter/RoleHierarchyVoterTest.php
+++ b/src/Symfony/Component/Security/Core/Tests/Authorization/Voter/RoleHierarchyVoterTest.php
@@ -12,19 +12,39 @@
namespace Symfony\Component\Security\Core\Tests\Authorization\Voter;
use Symfony\Component\Security\Core\Authorization\Voter\RoleHierarchyVoter;
+use Symfony\Component\Security\Core\Authorization\Voter\Vote;
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
use Symfony\Component\Security\Core\Role\RoleHierarchy;
class RoleHierarchyVoterTest extends RoleVoterTest
{
/**
+ * @dataProvider getGetVoteTests
+ */
+ public function testGetVoteUsingTokenThatReturnsRoleNames($roles, $attributes, $expected)
+ {
+ $voter = new RoleHierarchyVoter(new RoleHierarchy(['ROLE_FOO' => ['ROLE_FOOBAR']]));
+
+ $this->assertEquals($expected, $voter->getVote($this->getTokenWithRoleNames($roles), null, $attributes));
+ }
+
+ public function getGetVoteTests()
+ {
+ return array_merge(parent::getGetVoteTests(), [
+ [['ROLE_FOO'], ['ROLE_FOOBAR'], Vote::createGranted()],
+ ]);
+ }
+
+ /**
+ * @group legacy
+ *
* @dataProvider getVoteTests
*/
- public function testVoteUsingTokenThatReturnsRoleNames($roles, $attributes, $expected)
+ public function testVoteUsingTokenThatReturnsRoleNamesLegacy($roles, $attributes, $expected)
{
$voter = new RoleHierarchyVoter(new RoleHierarchy(['ROLE_FOO' => ['ROLE_FOOBAR']]));
- $this->assertSame($expected, $voter->vote($this->getTokenWithRoleNames($roles), null, $attributes));
+ $this->assertEquals($expected, $voter->vote($this->getTokenWithRoleNames($roles), null, $attributes));
}
public static function getVoteTests()
@@ -41,7 +61,7 @@ public function testVoteWithEmptyHierarchyUsingTokenThatReturnsRoleNames($roles,
{
$voter = new RoleHierarchyVoter(new RoleHierarchy([]));
- $this->assertSame($expected, $voter->vote($this->getTokenWithRoleNames($roles), null, $attributes));
+ $this->assertEquals($expected, $voter->getVote($this->getTokenWithRoleNames($roles), null, $attributes)->getAccess());
}
public static function getVoteWithEmptyHierarchyTests()
diff --git a/src/Symfony/Component/Security/Core/Tests/Authorization/Voter/RoleVoterTest.php b/src/Symfony/Component/Security/Core/Tests/Authorization/Voter/RoleVoterTest.php
index dfa0555652fba..3eb66d014c26f 100644
--- a/src/Symfony/Component/Security/Core/Tests/Authorization/Voter/RoleVoterTest.php
+++ b/src/Symfony/Component/Security/Core/Tests/Authorization/Voter/RoleVoterTest.php
@@ -12,21 +12,54 @@
namespace Symfony\Component\Security\Core\Tests\Authorization\Voter;
use PHPUnit\Framework\TestCase;
+use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait;
use Symfony\Component\Security\Core\Authentication\AuthenticationTrustResolver;
use Symfony\Component\Security\Core\Authentication\Token\AbstractToken;
use Symfony\Component\Security\Core\Authorization\Voter\AuthenticatedVoter;
use Symfony\Component\Security\Core\Authorization\Voter\RoleVoter;
+use Symfony\Component\Security\Core\Authorization\Voter\Vote;
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
class RoleVoterTest extends TestCase
{
+ use ExpectDeprecationTrait;
+
+ /**
+ * @dataProvider getGetVoteTests
+ */
+ public function testGetVoteUsingTokenThatReturnsRoleNames($roles, $attributes, $expected)
+ {
+ $voter = new RoleVoter();
+
+ $this->assertEquals($expected, $voter->getVote($this->getTokenWithRoleNames($roles), null, $attributes));
+ }
+
+ public function getGetVoteTests()
+ {
+ return [
+ [[], [], Vote::createAbstain()],
+ [[], ['FOO'], Vote::createAbstain()],
+ [[], ['ROLE_FOO'], Vote::createDenied()],
+ [['ROLE_FOO'], ['ROLE_FOO'], Vote::createGranted()],
+ [['ROLE_FOO'], ['FOO', 'ROLE_FOO'], Vote::createGranted()],
+ [['ROLE_BAR', 'ROLE_FOO'], ['ROLE_FOO'], Vote::createGranted()],
+
+ // Test mixed Types
+ [[], [[]], Vote::createAbstain()],
+ [[], [new \stdClass()], Vote::createAbstain()],
+ ];
+ }
+
/**
+ * @group legacy
+ *
* @dataProvider getVoteTests
*/
- public function testVoteUsingTokenThatReturnsRoleNames($roles, $attributes, $expected)
+ public function testVoteUsingTokenThatReturnsRoleNamesLegacy($roles, $attributes, $expected)
{
$voter = new RoleVoter();
+ $this->expectDeprecation('Since symfony/security-core 7.1: Method "%s::vote()" has been deprecated, use "%s::getVote()" instead.');
$this->assertSame($expected, $voter->vote($this->getTokenWithRoleNames($roles), null, $attributes));
}
diff --git a/src/Symfony/Component/Security/Core/Tests/Authorization/Voter/TraceableVoterTest.php b/src/Symfony/Component/Security/Core/Tests/Authorization/Voter/TraceableVoterTest.php
index d0f8ae08f97db..996e718bad554 100644
--- a/src/Symfony/Component/Security/Core/Tests/Authorization/Voter/TraceableVoterTest.php
+++ b/src/Symfony/Component/Security/Core/Tests/Authorization/Voter/TraceableVoterTest.php
@@ -12,15 +12,19 @@
namespace Symfony\Component\Security\Core\Tests\Authorization\Voter;
use PHPUnit\Framework\TestCase;
+use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\CacheableVoterInterface;
use Symfony\Component\Security\Core\Authorization\Voter\TraceableVoter;
+use Symfony\Component\Security\Core\Authorization\Voter\Vote;
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
use Symfony\Component\Security\Core\Event\VoteEvent;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
class TraceableVoterTest extends TestCase
{
+ use ExpectDeprecationTrait;
+
public function testGetDecoratedVoterClass()
{
$voter = $this->getMockBuilder(VoterInterface::class)->getMockForAbstractClass();
@@ -30,6 +34,39 @@ public function testGetDecoratedVoterClass()
}
public function testVote()
+ {
+ $voter = $this
+ ->getMockBuilder(VoterInterface::class)
+ ->onlyMethods(['vote'])
+ ->addMethods(['getVote'])
+ ->getMock();
+
+ $eventDispatcher = $this->getMockBuilder(EventDispatcherInterface::class)->getMockForAbstractClass();
+ $token = $this->getMockBuilder(TokenInterface::class)->getMockForAbstractClass();
+
+ $vote = Vote::createDenied();
+
+ $voter
+ ->expects($this->once())
+ ->method('getVote')
+ ->with($token, 'anysubject', ['attr1'])
+ ->willReturn($vote);
+
+ $eventDispatcher
+ ->expects($this->once())
+ ->method('dispatch')
+ ->with(new VoteEvent($voter, 'anysubject', ['attr1'], $vote), 'debug.security.authorization.vote');
+
+ $sut = new TraceableVoter($voter, $eventDispatcher);
+ $result = $sut->getVote($token, 'anysubject', ['attr1']);
+
+ $this->assertSame($vote, $result);
+ }
+
+ /**
+ * @group legacy
+ */
+ public function testVoteLegacy()
{
$voter = $this->getMockBuilder(VoterInterface::class)->getMockForAbstractClass();
@@ -48,6 +85,8 @@ public function testVote()
->with(new VoteEvent($voter, 'anysubject', ['attr1'], VoterInterface::ACCESS_DENIED), 'debug.security.authorization.vote');
$sut = new TraceableVoter($voter, $eventDispatcher);
+
+ $this->expectDeprecation('Since symfony/security-core 7.1: Method "%s::vote()" has been deprecated, use "%s::getVote()" instead.');
$result = $sut->vote($token, 'anysubject', ['attr1']);
$this->assertSame(VoterInterface::ACCESS_DENIED, $result);
diff --git a/src/Symfony/Component/Security/Core/Tests/Authorization/Voter/VoteTest.php b/src/Symfony/Component/Security/Core/Tests/Authorization/Voter/VoteTest.php
new file mode 100644
index 0000000000000..96bdbbfa0df80
--- /dev/null
+++ b/src/Symfony/Component/Security/Core/Tests/Authorization/Voter/VoteTest.php
@@ -0,0 +1,38 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Security\Core\Tests\Authorization\Voter;
+
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\Security\Core\Authorization\Voter\Vote;
+use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
+use Symfony\Component\Security\Core\Exception\InvalidArgumentException;
+
+class VoteTest extends TestCase
+{
+ public function testMessages()
+ {
+ $vote = new Vote(VoterInterface::ACCESS_GRANTED, 'foo');
+
+ $this->assertSame('foo', $vote->getMessage());
+
+ $vote->addMessage('bar');
+ $this->assertSame('foo, bar', $vote->getMessage());
+ }
+
+ public function testMessagesWithNotString()
+ {
+ $this->expectException(InvalidArgumentException::class);
+ $this->expectExceptionMessage('Message must be string, "bool" given.');
+
+ new Vote(VoterInterface::ACCESS_GRANTED, ['foo', true]);
+ }
+}
diff --git a/src/Symfony/Component/Security/Core/Tests/Authorization/Voter/VoterTest.php b/src/Symfony/Component/Security/Core/Tests/Authorization/Voter/VoterTest.php
index 5636340e6aea2..b70b3e066ae9f 100644
--- a/src/Symfony/Component/Security/Core/Tests/Authorization/Voter/VoterTest.php
+++ b/src/Symfony/Component/Security/Core/Tests/Authorization/Voter/VoterTest.php
@@ -12,12 +12,16 @@
namespace Symfony\Component\Security\Core\Tests\Authorization\Voter;
use PHPUnit\Framework\TestCase;
+use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
+use Symfony\Component\Security\Core\Authorization\Voter\Vote;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
class VoterTest extends TestCase
{
+ use ExpectDeprecationTrait;
+
protected TokenInterface $token;
protected function setUp(): void
@@ -30,6 +34,44 @@ public static function getTests(): array
$voter = new VoterTest_Voter();
$integerVoter = new IntegerVoterTest_Voter();
+ return [
+ [$voter, ['EDIT'], Vote::createGranted(), new \stdClass(), 'ACCESS_GRANTED if attribute and class are supported and attribute grants access'],
+ [$voter, ['CREATE'], Vote::createDenied(), new \stdClass(), 'ACCESS_DENIED if attribute and class are supported and attribute does not grant access'],
+
+ [$voter, ['DELETE', 'EDIT'], Vote::createGranted(), new \stdClass(), 'ACCESS_GRANTED if one attribute is supported and grants access'],
+ [$voter, ['DELETE', 'CREATE'], Vote::createDenied(), new \stdClass(), 'ACCESS_DENIED if one attribute is supported and denies access'],
+
+ [$voter, ['CREATE', 'EDIT'], Vote::createGranted(), new \stdClass(), 'ACCESS_GRANTED if one attribute grants access'],
+
+ [$voter, ['DELETE'], Vote::createAbstain(), new \stdClass(), 'ACCESS_ABSTAIN if no attribute is supported'],
+
+ [$voter, ['EDIT'], Vote::createAbstain(), new class() {}, 'ACCESS_ABSTAIN if class is not supported'],
+
+ [$voter, ['EDIT'], Vote::createAbstain(), null, 'ACCESS_ABSTAIN if object is null'],
+
+ [$voter, [], Vote::createAbstain(), new \stdClass(), 'ACCESS_ABSTAIN if no attributes were provided'],
+
+ [$voter, [new StringableAttribute()], Vote::createGranted(), new \stdClass(), 'ACCESS_GRANTED if attribute and class are supported and attribute grants access'],
+
+ [$voter, [new \stdClass()], Vote::createAbstain(), new \stdClass(), 'ACCESS_ABSTAIN if attributes were not strings'],
+
+ [$integerVoter, [42], Vote::createGranted(), new \stdClass(), 'ACCESS_GRANTED if attribute is an integer'],
+ ];
+ }
+
+ /**
+ * @dataProvider getTests
+ */
+ public function testGetVote(VoterInterface $voter, array $attributes, $expectedVote, $object, $message)
+ {
+ $this->assertEquals($expectedVote, $voter->getVote($this->token, $object, $attributes), $message);
+ }
+
+ public function getTestsLegacy()
+ {
+ $voter = new VoterLegacyTest_Voter();
+ $integerVoter = new IntegerVoterLegacyTest_Voter();
+
return [
[$voter, ['EDIT'], VoterInterface::ACCESS_GRANTED, new \stdClass(), 'ACCESS_GRANTED if attribute and class are supported and attribute grants access'],
[$voter, ['CREATE'], VoterInterface::ACCESS_DENIED, new \stdClass(), 'ACCESS_DENIED if attribute and class are supported and attribute does not grant access'],
@@ -56,22 +98,52 @@ public static function getTests(): array
}
/**
- * @dataProvider getTests
+ * @group legacy
+ *
+ * @dataProvider getTestsLegacy
*/
- public function testVote(VoterInterface $voter, array $attributes, $expectedVote, $object, $message)
+ public function testVoteLegacy(VoterInterface $voter, array $attributes, $expectedVote, $object, $message)
{
+ $this->expectDeprecation('Since symfony/security-core 7.1: Method "%s::vote()" has been deprecated, use "%s::getVote()" instead.');
$this->assertEquals($expectedVote, $voter->vote($this->token, $object, $attributes), $message);
}
- public function testVoteWithTypeError()
+ public function testVoteMessage()
+ {
+ $voter = new IntegerVoterVoteTest_Voter();
+ $vote = $voter->getVote($this->token, new \stdClass(), [43]);
+ $this->assertSame('foobar message', $vote->getMessage());
+ }
+
+ public function testVoteMessageMultipleAttributes()
+ {
+ $voter = new IntegerVoterVoteTest_Voter();
+ $vote = $voter->getVote($this->token, new \stdClass(), [43, 44]);
+ $this->assertSame('foobar message, foobar message', $vote->getMessage());
+ }
+
+ public function testGetVoteWithTypeError()
{
$this->expectException(\TypeError::class);
$this->expectExceptionMessage('Should error');
- (new TypeErrorVoterTest_Voter())->vote($this->token, new \stdClass(), ['EDIT']);
+ (new TypeErrorVoterTest_Voter())->getVote($this->token, new \stdClass(), ['EDIT']);
}
}
class VoterTest_Voter extends Voter
+{
+ protected function voteOnAttribute(string $attribute, mixed $object, TokenInterface $token): Vote
+ {
+ return 'EDIT' === $attribute ? Vote::createGranted() : Vote::createDenied();
+ }
+
+ protected function supports(string $attribute, $object): bool
+ {
+ return $object instanceof \stdClass && \in_array($attribute, ['EDIT', 'CREATE']);
+ }
+}
+
+class VoterLegacyTest_Voter extends Voter
{
protected function voteOnAttribute(string $attribute, $object, TokenInterface $token): bool
{
@@ -85,6 +157,19 @@ protected function supports(string $attribute, $object): bool
}
class IntegerVoterTest_Voter extends Voter
+{
+ protected function voteOnAttribute($attribute, $object, TokenInterface $token): Vote
+ {
+ return 42 === $attribute ? Vote::createGranted() : Vote::createDenied();
+ }
+
+ protected function supports($attribute, $object): bool
+ {
+ return $object instanceof \stdClass && \is_int($attribute);
+ }
+}
+
+class IntegerVoterLegacyTest_Voter extends Voter
{
protected function voteOnAttribute($attribute, $object, TokenInterface $token): bool
{
@@ -97,6 +182,23 @@ protected function supports($attribute, $object): bool
}
}
+class IntegerVoterVoteTest_Voter extends Voter
+{
+ protected function voteOnAttribute($attribute, $object, TokenInterface $token): Vote
+ {
+ if (42 === $attribute) {
+ return Vote::createGranted();
+ }
+
+ return Vote::createDenied('foobar message');
+ }
+
+ protected function supports($attribute, $object): bool
+ {
+ return $object instanceof \stdClass && \is_int($attribute);
+ }
+}
+
class TypeErrorVoterTest_Voter extends Voter
{
protected function voteOnAttribute($attribute, $object, TokenInterface $token): bool
diff --git a/src/Symfony/Component/Security/Core/Tests/Exception/AccessDeniedExceptionTest.php b/src/Symfony/Component/Security/Core/Tests/Exception/AccessDeniedExceptionTest.php
new file mode 100644
index 0000000000000..e536bd7132648
--- /dev/null
+++ b/src/Symfony/Component/Security/Core/Tests/Exception/AccessDeniedExceptionTest.php
@@ -0,0 +1,72 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\Security\Core\Tests\Exception;
+
+use PHPUnit\Framework\TestCase;
+use Symfony\Component\Security\Core\Authorization\AccessDecision;
+use Symfony\Component\Security\Core\Authorization\Voter\Vote;
+use Symfony\Component\Security\Core\Exception\AccessDeniedException;
+
+final class AccessDeniedExceptionTest extends TestCase
+{
+ /**
+ * @dataProvider getAccessDescisions
+ */
+ public function testSetAccessDecision(AccessDecision $accessDecision, string $expected)
+ {
+ $exception = new AccessDeniedException();
+ $exception->setAccessDecision($accessDecision);
+
+ $this->assertSame($expected, $exception->getMessage());
+ }
+
+ public function getAccessDescisions(): \Generator
+ {
+ yield [
+ AccessDecision::createDenied([
+ Vote::createDenied('foo'),
+ Vote::createDenied('bar'),
+ Vote::createDenied('baz'),
+ ]),
+ 'Access Denied. Decision messages are "foo" and "bar" and "baz"',
+ ];
+
+ yield [
+ AccessDecision::createDenied(),
+ 'Access Denied.',
+ ];
+
+ yield [
+ AccessDecision::createDenied([
+ Vote::createAbstain('foo'),
+ Vote::createDenied('bar'),
+ Vote::createAbstain('baz'),
+ ]),
+ 'Access Denied. Decision message is "bar"',
+ ];
+
+ yield [
+ AccessDecision::createGranted([
+ Vote::createDenied('foo'),
+ ]),
+ 'Access Denied. Decision message is "foo"',
+ ];
+
+ yield [
+ AccessDecision::createGranted([
+ Vote::createDenied(['foo', 'bar']),
+ Vote::createDenied(['baz', 'qux']),
+ ]),
+ 'Access Denied. Decision messages are "foo, bar" and "baz, qux"',
+ ];
+ }
+}
diff --git a/src/Symfony/Component/Security/Core/Tests/Fixtures/DummyVoter.php b/src/Symfony/Component/Security/Core/Tests/Fixtures/DummyVoter.php
index 1f923423a21ed..e32769e06b28c 100644
--- a/src/Symfony/Component/Security/Core/Tests/Fixtures/DummyVoter.php
+++ b/src/Symfony/Component/Security/Core/Tests/Fixtures/DummyVoter.php
@@ -12,6 +12,7 @@
namespace Symfony\Component\Security\Core\Tests\Fixtures;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
+use Symfony\Component\Security\Core\Authorization\Voter\Vote;
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
final class DummyVoter implements VoterInterface
@@ -19,4 +20,8 @@ final class DummyVoter implements VoterInterface
public function vote(TokenInterface $token, $subject, array $attributes): int
{
}
+
+ public function getVote(TokenInterface $token, mixed $subject, array $attributes): Vote
+ {
+ }
}
diff --git a/src/Symfony/Component/Security/Core/composer.json b/src/Symfony/Component/Security/Core/composer.json
index a923768be51da..7c0c4e165764d 100644
--- a/src/Symfony/Component/Security/Core/composer.json
+++ b/src/Symfony/Component/Security/Core/composer.json
@@ -26,6 +26,7 @@
"psr/cache": "^1.0|^2.0|^3.0",
"symfony/cache": "^6.4|^7.0",
"symfony/dependency-injection": "^6.4|^7.0",
+ "symfony/deprecation-contracts": "^2.5|^3.0",
"symfony/event-dispatcher": "^6.4|^7.0",
"symfony/expression-language": "^6.4|^7.0",
"symfony/http-foundation": "^6.4|^7.0",
diff --git a/src/Symfony/Component/Security/Http/Firewall/AccessListener.php b/src/Symfony/Component/Security/Http/Firewall/AccessListener.php
index 9f4337975c58b..e88d3032c9086 100644
--- a/src/Symfony/Component/Security/Http/Firewall/AccessListener.php
+++ b/src/Symfony/Component/Security/Http/Firewall/AccessListener.php
@@ -15,6 +15,7 @@
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\Security\Core\Authentication\Token\NullToken;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
+use Symfony\Component\Security\Core\Authorization\AccessDecision;
use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface;
use Symfony\Component\Security\Core\Authorization\Voter\AuthenticatedVoter;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
@@ -77,16 +78,23 @@ public function authenticate(RequestEvent $event): void
$token = $this->tokenStorage->getToken() ?? new NullToken();
- if (!$this->accessDecisionManager->decide($token, $attributes, $request, true)) {
- throw $this->createAccessDeniedException($request, $attributes);
+ if (method_exists($this->accessDecisionManager, 'getDecision')) {
+ $decision = $this->accessDecisionManager->getDecision($token, $attributes, $request, true);
+ } else {
+ $decision = $this->accessDecisionManager->decide($token, $attributes, $request, true) ? AccessDecision::createGranted() : AccessDecision::createDenied();
+ }
+
+ if ($decision->isDenied()) {
+ throw $this->createAccessDeniedException($request, $attributes, $decision);
}
}
- private function createAccessDeniedException(Request $request, array $attributes): AccessDeniedException
+ private function createAccessDeniedException(Request $request, array $attributes, AccessDecision $accessDecision): AccessDeniedException
{
$exception = new AccessDeniedException();
$exception->setAttributes($attributes);
$exception->setSubject($request);
+ $exception->setAccessDecision($accessDecision);
return $exception;
}
diff --git a/src/Symfony/Component/Security/Http/Firewall/SwitchUserListener.php b/src/Symfony/Component/Security/Http/Firewall/SwitchUserListener.php
index a8c7a652f6623..50d1584a99bbe 100644
--- a/src/Symfony/Component/Security/Http/Firewall/SwitchUserListener.php
+++ b/src/Symfony/Component/Security/Http/Firewall/SwitchUserListener.php
@@ -19,6 +19,7 @@
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Authentication\Token\SwitchUserToken;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
+use Symfony\Component\Security\Core\Authorization\AccessDecision;
use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use Symfony\Component\Security\Core\Exception\AuthenticationCredentialsNotFoundException;
@@ -168,9 +169,16 @@ private function attemptSwitchUser(Request $request, string $username): ?TokenIn
throw $e;
}
- if (false === $this->accessDecisionManager->decide($token, [$this->role], $user)) {
+ if (method_exists($this->accessDecisionManager, 'getDecision')) {
+ $decision = $this->accessDecisionManager->getDecision($token, [$this->role], $user);
+ } else {
+ $decision = $this->accessDecisionManager->decide($token, [$this->role], $user) ? AccessDecision::createGranted() : AccessDecision::createDenied();
+ }
+
+ if ($decision->isDenied()) {
$exception = new AccessDeniedException();
$exception->setAttributes($this->role);
+ $exception->setAccessDecision($decision);
throw $exception;
}
diff --git a/src/Symfony/Component/Security/Http/Tests/Firewall/AccessListenerTest.php b/src/Symfony/Component/Security/Http/Tests/Firewall/AccessListenerTest.php
index e9bc31587ffba..331248d17ee4a 100644
--- a/src/Symfony/Component/Security/Http/Tests/Firewall/AccessListenerTest.php
+++ b/src/Symfony/Component/Security/Http/Tests/Firewall/AccessListenerTest.php
@@ -20,6 +20,7 @@
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
+use Symfony\Component\Security\Core\Authorization\AccessDecision;
use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface;
use Symfony\Component\Security\Core\Authorization\Voter\AuthenticatedVoter;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
@@ -55,6 +56,58 @@ public function getCredentials(): mixed
->willReturn($token)
;
+ $accessDecisionManager = $this
+ ->getMockBuilder(AccessDecisionManagerInterface::class)
+ ->onlyMethods(['decide'])
+ ->addMethods(['getDecision'])
+ ->getMock();
+ $accessDecisionManager
+ ->expects($this->once())
+ ->method('getDecision')
+ ->with($this->equalTo($token), $this->equalTo(['foo' => 'bar']), $this->equalTo($request))
+ ->willReturn(AccessDecision::createDenied())
+ ;
+
+ $listener = new AccessListener(
+ $tokenStorage,
+ $accessDecisionManager,
+ $accessMap
+ );
+
+ $this->expectException(AccessDeniedException::class);
+
+ $listener(new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MAIN_REQUEST));
+ }
+
+ /**
+ * @group legacy
+ */
+ public function testHandleWhenTheAccessDecisionManagerDecidesToRefuseAccessLegacy()
+ {
+ $this->expectException(AccessDeniedException::class);
+ $request = new Request();
+
+ $accessMap = $this->createMock(AccessMapInterface::class);
+ $accessMap
+ ->expects($this->any())
+ ->method('getPatterns')
+ ->with($this->equalTo($request))
+ ->willReturn([['foo' => 'bar'], null])
+ ;
+
+ $token = new class() extends AbstractToken {
+ public function getCredentials(): mixed
+ {
+ }
+ };
+
+ $tokenStorage = $this->createMock(TokenStorageInterface::class);
+ $tokenStorage
+ ->expects($this->any())
+ ->method('getToken')
+ ->willReturn($token)
+ ;
+
$accessDecisionManager = $this->createMock(AccessDecisionManagerInterface::class);
$accessDecisionManager
->expects($this->once())
@@ -142,11 +195,15 @@ public function testHandleWhenTheSecurityTokenStorageHasNoToken()
->willReturn([['foo' => 'bar'], null])
;
- $accessDecisionManager = $this->createMock(AccessDecisionManagerInterface::class);
+ $accessDecisionManager = $this
+ ->getMockBuilder(AccessDecisionManagerInterface::class)
+ ->onlyMethods(['decide'])
+ ->addMethods(['getDecision'])
+ ->getMock();
$accessDecisionManager->expects($this->once())
- ->method('decide')
+ ->method('getDecision')
->with($this->isInstanceOf(NullToken::class))
- ->willReturn(false);
+ ->willReturn(AccessDecision::createDenied());
$listener = new AccessListener(
$tokenStorage,
@@ -172,11 +229,15 @@ public function testHandleWhenPublicAccessIsAllowed()
->willReturn([[AuthenticatedVoter::PUBLIC_ACCESS], null])
;
- $accessDecisionManager = $this->createMock(AccessDecisionManagerInterface::class);
+ $accessDecisionManager = $this
+ ->getMockBuilder(AccessDecisionManagerInterface::class)
+ ->onlyMethods(['decide'])
+ ->addMethods(['getDecision'])
+ ->getMock();
$accessDecisionManager->expects($this->once())
- ->method('decide')
+ ->method('getDecision')
->with($this->isInstanceOf(NullToken::class), [AuthenticatedVoter::PUBLIC_ACCESS])
- ->willReturn(true);
+ ->willReturn(AccessDecision::createGranted());
$listener = new AccessListener(
$tokenStorage,
@@ -202,11 +263,15 @@ public function testHandleWhenPublicAccessWhileAuthenticated()
->willReturn([[AuthenticatedVoter::PUBLIC_ACCESS], null])
;
- $accessDecisionManager = $this->createMock(AccessDecisionManagerInterface::class);
+ $accessDecisionManager = $this
+ ->getMockBuilder(AccessDecisionManagerInterface::class)
+ ->onlyMethods(['decide'])
+ ->addMethods(['getDecision'])
+ ->getMock();
$accessDecisionManager->expects($this->once())
- ->method('decide')
+ ->method('getDecision')
->with($this->equalTo($token), [AuthenticatedVoter::PUBLIC_ACCESS])
- ->willReturn(true);
+ ->willReturn(AccessDecision::createGranted());
$listener = new AccessListener(
$tokenStorage,
@@ -235,12 +300,16 @@ public function testHandleMWithultipleAttributesShouldBeHandledAsAnd()
$tokenStorage = new TokenStorage();
$tokenStorage->setToken($authenticatedToken);
- $accessDecisionManager = $this->createMock(AccessDecisionManagerInterface::class);
+ $accessDecisionManager = $this
+ ->getMockBuilder(AccessDecisionManagerInterface::class)
+ ->onlyMethods(['decide'])
+ ->addMethods(['getDecision'])
+ ->getMock();
$accessDecisionManager
->expects($this->once())
- ->method('decide')
+ ->method('getDecision')
->with($this->equalTo($authenticatedToken), $this->equalTo(['foo' => 'bar', 'bar' => 'baz']), $this->equalTo($request), true)
- ->willReturn(true)
+ ->willReturn(AccessDecision::createGranted())
;
$listener = new AccessListener(
diff --git a/src/Symfony/Component/Security/Http/Tests/Firewall/SwitchUserListenerTest.php b/src/Symfony/Component/Security/Http/Tests/Firewall/SwitchUserListenerTest.php
index 46da56485d529..c7b5215fde4c9 100644
--- a/src/Symfony/Component/Security/Http/Tests/Firewall/SwitchUserListenerTest.php
+++ b/src/Symfony/Component/Security/Http/Tests/Firewall/SwitchUserListenerTest.php
@@ -20,6 +20,7 @@
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;
use Symfony\Component\Security\Core\Authentication\Token\SwitchUserToken;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
+use Symfony\Component\Security\Core\Authorization\AccessDecision;
use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use Symfony\Component\Security\Core\Exception\AuthenticationCredentialsNotFoundException;
@@ -45,7 +46,11 @@ protected function setUp(): void
$this->tokenStorage = new TokenStorage();
$this->userProvider = new InMemoryUserProvider(['kuba' => []]);
$this->userChecker = $this->createMock(UserCheckerInterface::class);
- $this->accessDecisionManager = $this->createMock(AccessDecisionManagerInterface::class);
+ $this->accessDecisionManager = $this
+ ->getMockBuilder(AccessDecisionManagerInterface::class)
+ ->onlyMethods(['decide'])
+ ->addMethods(['getDecision'])
+ ->getMock();
$this->request = new Request();
$this->event = new RequestEvent($this->createMock(HttpKernelInterface::class), $this->request, HttpKernelInterface::MAIN_REQUEST);
}
@@ -145,8 +150,8 @@ public function testSwitchUserIsDisallowed()
$this->request->query->set('_switch_user', 'kuba');
$this->accessDecisionManager->expects($this->once())
- ->method('decide')->with($token, ['ROLE_ALLOWED_TO_SWITCH'])
- ->willReturn(false);
+ ->method('getDecision')->with($token, ['ROLE_ALLOWED_TO_SWITCH'])
+ ->willReturn(AccessDecision::createDenied());
$listener = new SwitchUserListener($this->tokenStorage, $this->userProvider, $this->userChecker, 'provider123', $this->accessDecisionManager);
@@ -163,7 +168,7 @@ public function testSwitchUserTurnsAuthenticationExceptionTo403()
$this->request->query->set('_switch_user', 'not-existing');
$this->accessDecisionManager->expects($this->never())
- ->method('decide');
+ ->method('getDecision');
$listener = new SwitchUserListener($this->tokenStorage, $this->userProvider, $this->userChecker, 'provider123', $this->accessDecisionManager);
@@ -180,8 +185,8 @@ public function testSwitchUser()
$this->request->query->set('_switch_user', 'kuba');
$this->accessDecisionManager->expects($this->once())
- ->method('decide')->with($token, ['ROLE_ALLOWED_TO_SWITCH'], $this->callback(fn ($user) => 'kuba' === $user->getUserIdentifier()))
- ->willReturn(true);
+ ->method('getDecision')->with($token, ['ROLE_ALLOWED_TO_SWITCH'], $this->callback(fn ($user) => 'kuba' === $user->getUserIdentifier()))
+ ->willReturn(AccessDecision::createGranted());
$this->userChecker->expects($this->once())
->method('checkPostAuth')->with($this->callback(fn ($user) => 'kuba' === $user->getUserIdentifier()));
@@ -194,6 +199,32 @@ public function testSwitchUser()
$this->assertInstanceOf(UsernamePasswordToken::class, $this->tokenStorage->getToken());
}
+ /**
+ * @group legacy
+ */
+ public function testSwitchUserLegacy()
+ {
+ $token = new UsernamePasswordToken(new InMemoryUser('username', '', ['ROLE_FOO']), 'key', ['ROLE_FOO']);
+
+ $this->tokenStorage->setToken($token);
+ $this->request->query->set('_switch_user', 'kuba');
+
+ $accessDecisionManager = $this->createMock(AccessDecisionManagerInterface::class);
+ $accessDecisionManager->expects($this->once())
+ ->method('decide')->with($token, ['ROLE_ALLOWED_TO_SWITCH'], $this->callback(function ($user) { return 'kuba' === $user->getUserIdentifier(); }))
+ ->willReturn(true);
+
+ $this->userChecker->expects($this->once())
+ ->method('checkPostAuth')->with($this->callback(fn ($user) => 'kuba' === $user->getUserIdentifier()));
+
+ $listener = new SwitchUserListener($this->tokenStorage, $this->userProvider, $this->userChecker, 'provider123', $accessDecisionManager);
+ $listener($this->event);
+
+ $this->assertSame([], $this->request->query->all());
+ $this->assertSame('', $this->request->server->get('QUERY_STRING'));
+ $this->assertInstanceOf(UsernamePasswordToken::class, $this->tokenStorage->getToken());
+ }
+
public function testSwitchUserAlreadySwitched()
{
$originalToken = new UsernamePasswordToken(new InMemoryUser('original', null, ['ROLE_FOO']), 'key', ['ROLE_FOO']);
@@ -206,8 +237,8 @@ public function testSwitchUserAlreadySwitched()
$targetsUser = $this->callback(fn ($user) => 'kuba' === $user->getUserIdentifier());
$this->accessDecisionManager->expects($this->once())
- ->method('decide')->with($originalToken, ['ROLE_ALLOWED_TO_SWITCH'], $targetsUser)
- ->willReturn(true);
+ ->method('getDecision')->with($originalToken, ['ROLE_ALLOWED_TO_SWITCH'], $targetsUser)
+ ->willReturn(AccessDecision::createGranted());
$this->userChecker->expects($this->once())
->method('checkPostAuth')->with($targetsUser);
@@ -232,8 +263,8 @@ public function testSwitchUserWorksWithFalsyUsernames()
$this->userProvider->createUser($user = new InMemoryUser('0', null));
$this->accessDecisionManager->expects($this->once())
- ->method('decide')->with($token, ['ROLE_ALLOWED_TO_SWITCH'])
- ->willReturn(true);
+ ->method('getDecision')->with($token, ['ROLE_ALLOWED_TO_SWITCH'])
+ ->willReturn(AccessDecision::createGranted());
$this->userChecker->expects($this->once())
->method('checkPostAuth')->with($this->callback(fn ($argUser) => $user->isEqualTo($argUser)));
@@ -259,8 +290,8 @@ public function testSwitchUserKeepsOtherQueryStringParameters()
$targetsUser = $this->callback(fn ($user) => 'kuba' === $user->getUserIdentifier());
$this->accessDecisionManager->expects($this->once())
- ->method('decide')->with($token, ['ROLE_ALLOWED_TO_SWITCH'], $targetsUser)
- ->willReturn(true);
+ ->method('getDecision')->with($token, ['ROLE_ALLOWED_TO_SWITCH'], $targetsUser)
+ ->willReturn(AccessDecision::createGranted());
$this->userChecker->expects($this->once())
->method('checkPostAuth')->with($targetsUser);
@@ -284,8 +315,8 @@ public function testSwitchUserWithReplacedToken()
$this->request->query->set('_switch_user', 'kuba');
$this->accessDecisionManager->expects($this->any())
- ->method('decide')->with($token, ['ROLE_ALLOWED_TO_SWITCH'], $this->callback(fn ($user) => 'kuba' === $user->getUserIdentifier()))
- ->willReturn(true);
+ ->method('getDecision')->with($token, ['ROLE_ALLOWED_TO_SWITCH'], $this->callback(fn ($user) => 'kuba' === $user->getUserIdentifier()))
+ ->willReturn(AccessDecision::createGranted());
$dispatcher = $this->createMock(EventDispatcherInterface::class);
$dispatcher
@@ -329,8 +360,8 @@ public function testSwitchUserStateless()
$targetsUser = $this->callback(fn ($user) => 'kuba' === $user->getUserIdentifier());
$this->accessDecisionManager->expects($this->once())
- ->method('decide')->with($token, ['ROLE_ALLOWED_TO_SWITCH'], $targetsUser)
- ->willReturn(true);
+ ->method('getDecision')->with($token, ['ROLE_ALLOWED_TO_SWITCH'], $targetsUser)
+ ->willReturn(AccessDecision::createGranted());
$this->userChecker->expects($this->once())
->method('checkPostAuth')->with($targetsUser);
diff --git a/src/Symfony/Component/Security/Http/composer.json b/src/Symfony/Component/Security/Http/composer.json
index 4bf955b99358c..df358900da9fa 100644
--- a/src/Symfony/Component/Security/Http/composer.json
+++ b/src/Symfony/Component/Security/Http/composer.json
@@ -43,6 +43,7 @@
"symfony/event-dispatcher": "<6.4",
"symfony/http-client-contracts": "<3.0",
"symfony/security-bundle": "<6.4",
+ "symfony/security-core": "<7.1",
"symfony/security-csrf": "<6.4"
},
"autoload": {