Skip to content

[Security] Add the ability for voter to return decision reason #43147

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 36 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
81f7499
Add the ability for voter to return decision reason
yellow1912 Sep 23, 2021
85a9b3d
Create mock with DecisionManager class
yellow1912 Sep 23, 2021
293a057
Fix phpunit test errors
yellow1912 Sep 23, 2021
66d49ac
Standardize Voter code
yellow1912 Sep 23, 2021
b43c6e5
Remove AccessTrait
yellow1912 Sep 23, 2021
cba2283
Change $access property to private as requested
yellow1912 Sep 23, 2021
6675182
Remove bool return to avoid BC
yellow1912 Sep 23, 2021
c15b796
Fix return typehint for decide method
yellow1912 Sep 23, 2021
7e5feef
Switch to using constructor for Vote
yellow1912 Sep 23, 2021
55bc4cd
Fix AccessDeniedException
yellow1912 Sep 23, 2021
219a3be
Fix coding standards
yellow1912 Sep 23, 2021
8c5fcb0
Fix test results
yellow1912 Sep 23, 2021
d970920
Fix deprecation to match with test
yellow1912 Sep 23, 2021
a9578c4
Fix decide method deprecation test matching
yellow1912 Sep 23, 2021
1357b53
Fix tests in AccessListenerTest
yellow1912 Sep 23, 2021
b949317
Add missing AbstractToken in AccessListenerTest
yellow1912 Sep 23, 2021
6b9ec12
getDecisionLog return object, not boolean
yellow1912 Sep 23, 2021
dfec312
Attempt to fix test in AccessListenerTest
yellow1912 Sep 24, 2021
95e9682
Fix testAccessDecisionManagerCalledByVoter
yellow1912 Sep 24, 2021
282c972
Add mising class in test
yellow1912 Sep 24, 2021
5993a1a
Fix coding standards
yellow1912 Sep 24, 2021
04159fd
Fix issue with final class mock
yellow1912 Sep 24, 2021
8b4430d
Fix issue with final class mock
yellow1912 Sep 24, 2021
e2212f6
Fix issue with final class mock
yellow1912 Sep 24, 2021
4f52a2c
Update version in deprecation message
yellow1912 Sep 24, 2021
de1f7e3
Fix BC with VoteEvent
yellow1912 Sep 24, 2021
7d4e955
Fix deprecation version
yellow1912 Sep 24, 2021
7589a16
Rename vote $reason to $message, $parameters to $context
yellow1912 Sep 24, 2021
8aacd94
Add back return typehint in phpdoc
yellow1912 Sep 24, 2021
d776986
Fix coding standard
yellow1912 Sep 24, 2021
991857d
Add more checkings for deprecation notices inside tests
yellow1912 Sep 26, 2021
504ec26
Merge test code with latest 5.4
yellow1912 Sep 26, 2021
9d59fdd
Allow getAccessDecision to return null in AccessDeniedException
yellow1912 Sep 27, 2021
7a4dc72
Merge security.html.twig with new changes
yellow1912 Oct 15, 2021
93c0c10
Fix coding standards
yellow1912 Oct 17, 2021
5ebf1b5
Add missing dot to comply with standards
yellow1912 Oct 17, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,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;
Expand Down Expand Up @@ -237,10 +238,22 @@ protected function isGranted($attribute, $subject = null): bool
*/
protected function denyAccessUnlessGranted($attribute, $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;
}
Expand Down Expand Up @@ -411,7 +424,7 @@ protected function getUser()
return null;
}

// @deprecated since 5.4, $user will always be a UserInterface instance
// @deprecated since Symfony 5.4, $user will always be a UserInterface instance
if (!\is_object($user = $token->getUser())) {
// e.g. anonymous authentication
return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ public function __construct(TraceableAccessDecisionManager $traceableAccessDecis
*/
public function onVoterVote(VoteEvent $event)
{
$this->traceableAccessDecisionManager->addVoterVote($event->getVoter(), $event->getAttributes(), $event->getVote());
$this->traceableAccessDecisionManager->addVoterVote($event->getVoter(), $event->getAttributes(), $event->getVoteDecision());
}

public static function getSubscribedEvents(): array
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -29,10 +30,33 @@ public function testOnVoterVote()
->setMethods(['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()
->setMethods(['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));
Expand Down
118 changes: 118 additions & 0 deletions src/Symfony/Component/Security/Core/Authorization/AccessDecision.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <danymaillard93b@gmail.com>
*/
final class AccessDecision
{
/** @var int One of the VoterInterface::ACCESS_* constants */
private $access;

/** @var Vote[] */
private $votes = [];

/**
* @param int $access One of the VoterInterface::ACCESS_* constants
* @param Vote[] $votes
*/
private function __construct(int $access, array $votes = [])
{
$this->access = $access;
$this->votes = $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 function (Vote $vote) use ($access): bool {
return $vote->getAccess() === $access;
});
}
}
Loading