-
-
Notifications
You must be signed in to change notification settings - Fork 9.7k
[Security] Be able to know the reasons of the denied access #35592
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
Changes from all commits
26a8381
868b9ff
edfc392
ec41df2
842e59e
7339108
8ef8d19
c8a360c
c4cfb5f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
<?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\AccessTrait; | ||
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 | ||
{ | ||
use AccessTrait; | ||
|
||
/** @var Vote[] */ | ||
private $votes = []; | ||
|
||
/** | ||
* @param int $access One of the VoterInterface::ACCESS_* constants | ||
* @param Vote[] $votes | ||
*/ | ||
private function __construct(int $access, array $votes = []) | ||
maidmaid marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ | ||
$this->access = $access; | ||
$this->votes = $votes; | ||
} | ||
|
||
/** | ||
* @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); | ||
} | ||
|
||
private function getVotesByAccess(int $access): array | ||
{ | ||
return array_filter($this->votes, function (Vote $vote) use ($access) { return $vote->getAccess() === $access; }); | ||
maidmaid marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
} |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -12,6 +12,7 @@ | |||||
namespace Symfony\Component\Security\Core\Authorization; | ||||||
|
||||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; | ||||||
use Symfony\Component\Security\Core\Authorization\Voter\Vote; | ||||||
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface; | ||||||
use Symfony\Component\Security\Core\Exception\InvalidArgumentException; | ||||||
|
||||||
|
@@ -72,26 +73,27 @@ public function decide(TokenInterface $token, array $attributes, $object = null) | |||||
* If all voters abstained from voting, the decision will be based on the | ||||||
* allowIfAllAbstainDecisions property value (defaults to false). | ||||||
*/ | ||||||
private function decideAffirmative(TokenInterface $token, array $attributes, $object = null): bool | ||||||
private function decideAffirmative(TokenInterface $token, array $attributes, $object = null): AccessDecision | ||||||
{ | ||||||
$votes = []; | ||||||
$deny = 0; | ||||||
foreach ($this->voters as $voter) { | ||||||
$result = $voter->vote($token, $object, $attributes); | ||||||
$votes[] = $vote = $this->vote($voter, $token, $object, $attributes); | ||||||
|
||||||
if (VoterInterface::ACCESS_GRANTED === $result) { | ||||||
return true; | ||||||
if ($vote->isGranted()) { | ||||||
return AccessDecision::createGranted($votes); | ||||||
} | ||||||
|
||||||
if (VoterInterface::ACCESS_DENIED === $result) { | ||||||
if ($vote->isDenied()) { | ||||||
++$deny; | ||||||
} | ||||||
} | ||||||
|
||||||
if ($deny > 0) { | ||||||
return false; | ||||||
return AccessDecision::createDenied($votes); | ||||||
} | ||||||
|
||||||
return $this->allowIfAllAbstainDecisions; | ||||||
return $this->decideIfAllAbstainDecisions(); | ||||||
} | ||||||
|
||||||
/** | ||||||
|
@@ -108,33 +110,37 @@ private function decideAffirmative(TokenInterface $token, array $attributes, $ob | |||||
* If all voters abstained from voting, the decision will be based on the | ||||||
* allowIfAllAbstainDecisions property value (defaults to false). | ||||||
*/ | ||||||
private function decideConsensus(TokenInterface $token, array $attributes, $object = null): bool | ||||||
private function decideConsensus(TokenInterface $token, array $attributes, $object = null): AccessDecision | ||||||
{ | ||||||
$votes = []; | ||||||
$grant = 0; | ||||||
$deny = 0; | ||||||
foreach ($this->voters as $voter) { | ||||||
$result = $voter->vote($token, $object, $attributes); | ||||||
$votes[] = $vote = $this->vote($voter, $token, $object, $attributes); | ||||||
|
||||||
if (VoterInterface::ACCESS_GRANTED === $result) { | ||||||
if ($vote->isGranted()) { | ||||||
++$grant; | ||||||
} elseif (VoterInterface::ACCESS_DENIED === $result) { | ||||||
} elseif ($vote->isDenied()) { | ||||||
++$deny; | ||||||
} | ||||||
} | ||||||
|
||||||
if ($grant > $deny) { | ||||||
return true; | ||||||
return AccessDecision::createGranted($votes); | ||||||
} | ||||||
|
||||||
if ($deny > $grant) { | ||||||
return false; | ||||||
return AccessDecision::createDenied($votes); | ||||||
} | ||||||
|
||||||
if ($grant > 0) { | ||||||
return $this->allowIfEqualGrantedDeniedDecisions; | ||||||
return $this->allowIfEqualGrantedDeniedDecisions | ||||||
? AccessDecision::createGranted() | ||||||
: AccessDecision::createDenied() | ||||||
; | ||||||
} | ||||||
|
||||||
return $this->allowIfAllAbstainDecisions; | ||||||
return $this->decideIfAllAbstainDecisions(); | ||||||
} | ||||||
|
||||||
/** | ||||||
|
@@ -143,29 +149,30 @@ private function decideConsensus(TokenInterface $token, array $attributes, $obje | |||||
* If all voters abstained from voting, the decision will be based on the | ||||||
* allowIfAllAbstainDecisions property value (defaults to false). | ||||||
*/ | ||||||
private function decideUnanimous(TokenInterface $token, array $attributes, $object = null): bool | ||||||
private function decideUnanimous(TokenInterface $token, array $attributes, $object = null): AccessDecision | ||||||
{ | ||||||
$votes = []; | ||||||
$grant = 0; | ||||||
foreach ($this->voters as $voter) { | ||||||
foreach ($attributes as $attribute) { | ||||||
$result = $voter->vote($token, $object, [$attribute]); | ||||||
$votes[] = $vote = $this->vote($voter, $token, $object, [$attribute]); | ||||||
|
||||||
if (VoterInterface::ACCESS_DENIED === $result) { | ||||||
return false; | ||||||
if ($vote->isDenied()) { | ||||||
return AccessDecision::createDenied($votes); | ||||||
} | ||||||
|
||||||
if (VoterInterface::ACCESS_GRANTED === $result) { | ||||||
if ($vote->isGranted()) { | ||||||
++$grant; | ||||||
} | ||||||
} | ||||||
} | ||||||
|
||||||
// no deny votes | ||||||
if ($grant > 0) { | ||||||
return true; | ||||||
return AccessDecision::createGranted($votes); | ||||||
} | ||||||
|
||||||
return $this->allowIfAllAbstainDecisions; | ||||||
return $this->decideIfAllAbstainDecisions(); | ||||||
} | ||||||
|
||||||
/** | ||||||
|
@@ -191,4 +198,22 @@ private function decidePriority(TokenInterface $token, array $attributes, $objec | |||||
|
||||||
return $this->allowIfAllAbstainDecisions; | ||||||
} | ||||||
|
||||||
private function decideIfAllAbstainDecisions(): AccessDecision | ||||||
{ | ||||||
return $this->allowIfAllAbstainDecisions | ||||||
? AccessDecision::createGranted() | ||||||
: AccessDecision::createDenied() | ||||||
; | ||||||
} | ||||||
|
||||||
private function vote(VoterInterface $voter, TokenInterface $token, $subject, array $attributes): Vote | ||||||
{ | ||||||
if (\is_int($vote = $voter->vote($token, $subject, $attributes))) { | ||||||
trigger_deprecation('symfony/security', 5.1, 'Returning an int from the "%s::vote()" method is deprecated. Return a "%s" object instead.', \get_class($this->voter), Vote::class); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
$vote = Vote::create($vote); | ||||||
} | ||||||
|
||||||
return $vote; | ||||||
} | ||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -26,7 +26,7 @@ interface AccessDecisionManagerInterface | |
* @param array $attributes An array of attributes associated with the method being invoked | ||
* @param object $object The object to secure | ||
* | ||
* @return bool true if the access is granted, false otherwise | ||
* @return bool|AccessDecision Returning a boolean is deprecated since Symfony 5.1. Return an AccessDecision object instead. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This interface is not internal, so we should also be BC with the usage of the method AFAIK. I don't think we currently preserve backwards compatibility in e.g. this example: if (!$accessDecisionManager->decide(...)) {
throw new AccessDeniedException();
} If an |
||
*/ | ||
public function decide(TokenInterface $token, array $attributes, $object = null); | ||
} |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -29,6 +29,8 @@ class AuthorizationChecker implements AuthorizationCheckerInterface | |||||
private $accessDecisionManager; | ||||||
private $authenticationManager; | ||||||
private $alwaysAuthenticate; | ||||||
/** @var AccessDecision */ | ||||||
private $lastAccessDecision; | ||||||
|
||||||
public function __construct(TokenStorageInterface $tokenStorage, AuthenticationManagerInterface $authenticationManager, AccessDecisionManagerInterface $accessDecisionManager, bool $alwaysAuthenticate = false) | ||||||
{ | ||||||
|
@@ -53,6 +55,19 @@ final public function isGranted($attribute, $subject = null): bool | |||||
$this->tokenStorage->setToken($token = $this->authenticationManager->authenticate($token)); | ||||||
} | ||||||
|
||||||
return $this->accessDecisionManager->decide($token, [$attribute], $subject); | ||||||
$this->lastAccessDecision = $this->accessDecisionManager->decide($token, [$attribute], $subject); | ||||||
|
||||||
if (\is_bool($this->lastAccessDecision)) { | ||||||
trigger_deprecation('symfony/security', 5.1, 'Returning a boolean from the "%s::decide()" method is deprecated. Return an "%s" object instead', \get_class($this->accessDecisionManager), AccessDecision::class); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
|
||||||
return $this->lastAccessDecision; | ||||||
} | ||||||
|
||||||
return $this->lastAccessDecision->isGranted(); | ||||||
} | ||||||
|
||||||
public function getLastAccessDecision(): AccessDecision | ||||||
{ | ||||||
return $this->lastAccessDecision; | ||||||
} | ||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
<?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\Voter; | ||
|
||
/** | ||
* @author Dany Maillard <danymaillard93b@gmail.com> | ||
*/ | ||
trait AccessTrait | ||
{ | ||
/** @var int One of the VoterInterface::ACCESS_* constants */ | ||
protected $access; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should be private. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Already discussed here #35592 (comment). Ok for you ? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not sure to understand the rationale there. |
||
|
||
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; | ||
} | ||
} |
Uh oh!
There was an error while loading. Please reload this page.