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": {
pFad - Phonifier reborn
Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies:
Alternative Proxy
pFad Proxy
pFad v3 Proxy
pFad v4 Proxy