Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commitfcc69a6

Browse files
[Security] Add ability for voters to explain their vote
1 parentd824d53 commitfcc69a6

File tree

27 files changed

+330
-120
lines changed

27 files changed

+330
-120
lines changed

‎src/Symfony/Bridge/Twig/Extension/SecurityExtension.php

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
namespaceSymfony\Bridge\Twig\Extension;
1313

1414
useSymfony\Component\Security\Acl\Voter\FieldVote;
15+
useSymfony\Component\Security\Core\Authorization\AccessDecision;
1516
useSymfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
1617
useSymfony\Component\Security\Core\Authorization\UserAuthorizationCheckerInterface;
1718
useSymfony\Component\Security\Core\Exception\AuthenticationCredentialsNotFoundException;
@@ -34,7 +35,7 @@ public function __construct(
3435
) {
3536
}
3637

37-
publicfunctionisGranted(mixed$role,mixed$object =null, ?string$field =null):bool
38+
publicfunctionisGranted(mixed$role,mixed$object =null, ?string$field =null, ?AccessDecision$accessDecision =null):bool
3839
{
3940
if (null ===$this->securityChecker) {
4041
returnfalse;
@@ -47,15 +48,23 @@ public function isGranted(mixed $role, mixed $object = null, ?string $field = nu
4748

4849
$object =newFieldVote($object,$field);
4950
}
51+
if (!class_exists(AccessDecision::class)) {
52+
try {
53+
return$this->securityChecker->isGranted($role,$object);
54+
}catch (AuthenticationCredentialsNotFoundException) {
55+
returnfalse;
56+
}
57+
}
58+
$accessDecision ??=newAccessDecision();
5059

5160
try {
52-
return$this->securityChecker->isGranted($role,$object);
61+
return$accessDecision->isGranted =$this->securityChecker->isGranted($role,$object,$accessDecision);
5362
}catch (AuthenticationCredentialsNotFoundException) {
54-
returnfalse;
63+
return$accessDecision->isGranted =false;
5564
}
5665
}
5766

58-
publicfunctionisGrantedForUser(UserInterface$user,mixed$attribute,mixed$subject =null, ?string$field =null):bool
67+
publicfunctionisGrantedForUser(UserInterface$user,mixed$attribute,mixed$subject =null, ?string$field =null, ?AccessDecision$accessDecision =null):bool
5968
{
6069
if (!$this->userSecurityChecker) {
6170
thrownew \LogicException(\sprintf('An instance of "%s" must be provided to use "%s()".', UserAuthorizationCheckerInterface::class,__METHOD__));
@@ -68,8 +77,20 @@ public function isGrantedForUser(UserInterface $user, mixed $attribute, mixed $s
6877

6978
$subject =newFieldVote($subject,$field);
7079
}
80+
if (!class_exists(AccessDecision::class)) {
81+
try {
82+
return$this->userSecurityChecker->isGrantedForUser($user,$attribute,$subject);
83+
}catch (AuthenticationCredentialsNotFoundException) {
84+
returnfalse;
85+
}
86+
}
87+
$accessDecision ??=newAccessDecision();
7188

72-
return$this->userSecurityChecker->isGrantedForUser($user,$attribute,$subject);
89+
try {
90+
return$accessDecision->isGranted =$this->userSecurityChecker->isGrantedForUser($user,$attribute,$subject,$accessDecision);
91+
}catch (AuthenticationCredentialsNotFoundException) {
92+
return$accessDecision->isGranted =false;
93+
}
7394
}
7495

7596
publicfunctiongetImpersonateExitUrl(?string$exitTo =null):string

‎src/Symfony/Bundle/FrameworkBundle/Controller/AbstractController.php

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
useSymfony\Component\Routing\Generator\UrlGeneratorInterface;
3636
useSymfony\Component\Routing\RouterInterface;
3737
useSymfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
38+
useSymfony\Component\Security\Core\Authorization\AccessDecision;
3839
useSymfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
3940
useSymfony\Component\Security\Core\Exception\AccessDeniedException;
4041
useSymfony\Component\Security\Core\User\UserInterface;
@@ -202,6 +203,21 @@ protected function isGranted(mixed $attribute, mixed $subject = null): bool
202203
return$this->container->get('security.authorization_checker')->isGranted($attribute,$subject);
203204
}
204205

206+
/**
207+
* Checks if the attribute is granted against the current authentication token and optionally supplied subject.
208+
*/
209+
protectedfunctiongetAccessDecision(mixed$attribute,mixed$subject =null):AccessDecision
210+
{
211+
if (!$this->container->has('security.authorization_checker')) {
212+
thrownew \LogicException('The SecurityBundle is not registered in your application. Try running "composer require symfony/security-bundle".');
213+
}
214+
215+
$accessDecision =newAccessDecision();
216+
$accessDecision->isGranted =$this->container->get('security.authorization_checker')->isGranted($attribute,$subject,$accessDecision);
217+
218+
return$accessDecision;
219+
}
220+
205221
/**
206222
* Throws an exception unless the attribute is granted against the current authentication token and optionally
207223
* supplied subject.
@@ -210,12 +226,24 @@ protected function isGranted(mixed $attribute, mixed $subject = null): bool
210226
*/
211227
protectedfunctiondenyAccessUnlessGranted(mixed$attribute,mixed$subject =null,string$message ='Access Denied.'):void
212228
{
213-
if (!$this->isGranted($attribute,$subject)) {
214-
$exception =$this->createAccessDeniedException($message);
215-
$exception->setAttributes([$attribute]);
216-
$exception->setSubject($subject);
229+
if (class_exists(AccessDecision::class)) {
230+
$accessDecision =$this->getAccessDecision($attribute,$subject);
231+
$isGranted =$accessDecision->isGranted;
232+
}else {
233+
$accessDecision =null;
234+
$isGranted =$this->isGranted($attribute,$subject);
235+
}
236+
237+
if (!$isGranted) {
238+
$e =$this->createAccessDeniedException(3 >\func_num_args() &&$accessDecision ?$accessDecision->getMessage() :$message);
239+
$e->setAttributes([$attribute]);
240+
$e->setSubject($subject);
241+
242+
if ($accessDecision) {
243+
$e->setAccessDecision($accessDecision);
244+
}
217245

218-
throw$exception;
246+
throw$e;
219247
}
220248
}
221249

‎src/Symfony/Bundle/SecurityBundle/DataCollector/SecurityDataCollector.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,7 @@ public function collect(Request $request, Response $response, ?\Throwable $excep
138138

139139
// collect voter details
140140
$decisionLog =$this->accessDecisionManager->getDecisionLog();
141+
141142
foreach ($decisionLogas$key =>$log) {
142143
$decisionLog[$key]['voter_details'] = [];
143144
foreach ($log['voterDetails']as$voterDetail) {
@@ -147,6 +148,7 @@ public function collect(Request $request, Response $response, ?\Throwable $excep
147148
'class' =>$classData,
148149
'attributes' =>$voterDetail['attributes'],// Only displayed for unanimous strategy
149150
'vote' =>$voterDetail['vote'],
151+
'reasons' =>$voterDetail['reasons'] ?? [],
150152
];
151153
}
152154
unset($decisionLog[$key]['voterDetails']);

‎src/Symfony/Bundle/SecurityBundle/EventListener/VoteListener.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ public function __construct(
3131

3232
publicfunctiononVoterVote(VoteEvent$event):void
3333
{
34-
$this->traceableAccessDecisionManager->addVoterVote($event->getVoter(),$event->getAttributes(),$event->getVote());
34+
$this->traceableAccessDecisionManager->addVoterVote($event->getVoter(),$event->getAttributes(),$event->getVote(),$event->getReasons());
3535
}
3636

3737
publicstaticfunctiongetSubscribedEvents():array

‎src/Symfony/Bundle/SecurityBundle/Resources/views/Collector/security.html.twig

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -571,14 +571,17 @@
571571
{%endif %}
572572
<tdclass="font-normal text-small">
573573
{%ifvoter_detail['vote']==constant('Symfony\\Component\\Security\\Core\\Authorization\\Voter\\VoterInterface::ACCESS_GRANTED') %}
574-
ACCESSGRANTED
574+
GRANTED
575575
{%elseifvoter_detail['vote']==constant('Symfony\\Component\\Security\\Core\\Authorization\\Voter\\VoterInterface::ACCESS_ABSTAIN') %}
576-
ACCESSABSTAIN
576+
ABSTAIN
577577
{%elseifvoter_detail['vote']==constant('Symfony\\Component\\Security\\Core\\Authorization\\Voter\\VoterInterface::ACCESS_DENIED') %}
578-
ACCESSDENIED
578+
DENIED
579579
{%else %}
580580
unknown ({{voter_detail['vote'] }})
581581
{%endif %}
582+
{%ifvoter_detail['reasons']is notempty %}
583+
<br>{{voter_detail['reasons'] | join('<br>') }}
584+
{%endif %}
582585
</td>
583586
</tr>
584587
{%endfor %}

‎src/Symfony/Bundle/SecurityBundle/Security.php

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
useSymfony\Component\HttpFoundation\Response;
1818
useSymfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
1919
useSymfony\Component\Security\Core\Authentication\Token\TokenInterface;
20+
useSymfony\Component\Security\Core\Authorization\AccessDecision;
2021
useSymfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
2122
useSymfony\Component\Security\Core\Authorization\UserAuthorizationCheckerInterface;
2223
useSymfony\Component\Security\Core\Exception\LogicException;
@@ -58,10 +59,10 @@ public function getUser(): ?UserInterface
5859
/**
5960
* Checks if the attributes are granted against the current authentication token and optionally supplied subject.
6061
*/
61-
publicfunctionisGranted(mixed$attributes,mixed$subject =null):bool
62+
publicfunctionisGranted(mixed$attributes,mixed$subject =null,AccessDecision$accessDecision =newAccessDecision()):bool
6263
{
63-
return$this->container->get('security.authorization_checker')
64-
->isGranted($attributes,$subject);
64+
return$accessDecision->isGranted =$this->container->get('security.authorization_checker')
65+
->isGranted($attributes,$subject,$accessDecision);
6566
}
6667

6768
publicfunctiongetToken(): ?TokenInterface
@@ -154,10 +155,10 @@ public function logout(bool $validateCsrfToken = true): ?Response
154155
*
155156
* This should be used over isGranted() when checking permissions against a user that is not currently logged in or while in a CLI context.
156157
*/
157-
publicfunctionisGrantedForUser(UserInterface$user,mixed$attribute,mixed$subject =null):bool
158+
publicfunctionisGrantedForUser(UserInterface$user,mixed$attribute,mixed$subject =null,AccessDecision$accessDecision =newAccessDecision()):bool
158159
{
159-
return$this->container->get('security.user_authorization_checker')
160-
->isGrantedForUser($user,$attribute,$subject);
160+
return$accessDecision->isGranted =$this->container->get('security.user_authorization_checker')
161+
->isGrantedForUser($user,$attribute,$subject,$accessDecision);
161162
}
162163

163164
privatefunctiongetAuthenticator(?string$authenticatorName,string$firewallName):AuthenticatorInterface
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespaceSymfony\Component\Security\Core\Authorization;
13+
14+
useSymfony\Component\Security\Core\Authorization\Voter\Vote;
15+
useSymfony\Component\Security\Core\Authorization\Voter\VoterInterface;
16+
17+
/**
18+
* Contains the access verdict and all the related votes.
19+
*
20+
* @author Dany Maillard <danymaillard93b@gmail.com>
21+
* @author Roman JOLY <eltharin18@outlook.fr>
22+
* @author Nicolas Grekas <p@tchwork.com>
23+
*/
24+
finalclass AccessDecision
25+
{
26+
/**
27+
* @var class-string<AccessDecisionStrategyInterface>|string|null
28+
*/
29+
public ?string$strategy =null;
30+
31+
publicbool$isGranted;
32+
33+
/**
34+
* @var Vote[]
35+
*/
36+
public$votes = [];
37+
38+
publicfunctiongetMessage():string
39+
{
40+
$message =$this->isGranted ?'Access granted.' :'Access denied.';
41+
$access =$this->isGranted ? VoterInterface::ACCESS_GRANTED : VoterInterface::ACCESS_DENIED;
42+
43+
if ($this->votes) {
44+
foreach ($this->votesas$vote) {
45+
if ($vote->result !==$access) {
46+
continue;
47+
}
48+
foreach ($vote->reasonsas$reason) {
49+
$message .=''.$reason;
50+
}
51+
}
52+
}
53+
54+
return$message;
55+
}
56+
}

‎src/Symfony/Component/Security/Core/Authorization/AccessDecisionManager.php

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
useSymfony\Component\Security\Core\Authorization\Strategy\AccessDecisionStrategyInterface;
1616
useSymfony\Component\Security\Core\Authorization\Strategy\AffirmativeStrategy;
1717
useSymfony\Component\Security\Core\Authorization\Voter\CacheableVoterInterface;
18+
useSymfony\Component\Security\Core\Authorization\Voter\TraceableVoter;
19+
useSymfony\Component\Security\Core\Authorization\Voter\Vote;
1820
useSymfony\Component\Security\Core\Authorization\Voter\VoterInterface;
1921
useSymfony\Component\Security\Core\Exception\InvalidArgumentException;
2022

@@ -49,35 +51,49 @@ public function __construct(
4951
/**
5052
* @param bool $allowMultipleAttributes Whether to allow passing multiple values to the $attributes array
5153
*/
52-
publicfunctiondecide(TokenInterface$token,array$attributes,mixed$object =null,bool$allowMultipleAttributes =false):bool
54+
publicfunctiondecide(TokenInterface$token,array$attributes,mixed$object =null,bool|AccessDecision$accessDecision =newAccessDecision(),bool$allowMultipleAttributes =false):bool
5355
{
56+
if (\is_bool($accessDecision)) {
57+
$allowMultipleAttributes =$accessDecision;
58+
$accessDecision =newAccessDecision();
59+
}
60+
5461
// Special case for AccessListener, do not remove the right side of the condition before 6.0
5562
if (\count($attributes) >1 && !$allowMultipleAttributes) {
5663
thrownewInvalidArgumentException(\sprintf('Passing more than one Security attribute to "%s()" is not supported.',__METHOD__));
5764
}
5865

59-
return$this->strategy->decide(
60-
$this->collectResults($token,$attributes,$object)
66+
$accessDecision->strategy =$this->strategyinstanceof \Stringable ?$this->strategy :get_debug_type($this->strategy);
67+
68+
return$accessDecision->isGranted =$this->strategy->decide(
69+
$this->collectResults($token,$attributes,$object,$accessDecision)
6170
);
6271
}
6372

6473
/**
65-
* @return \Traversable<int, int>
74+
* @return \Traversable<VoterInterface::ACCESS_*>
6675
*/
67-
privatefunctioncollectResults(TokenInterface$token,array$attributes,mixed$object):\Traversable
76+
privatefunctioncollectResults(TokenInterface$token,array$attributes,mixed$object,AccessDecision$accessDecision):\Traversable
6877
{
6978
foreach ($this->getVoters($attributes,$object)as$voter) {
70-
$result =$voter->vote($token,$object,$attributes);
79+
$vote =newVote();
80+
$result =$voter->vote($token,$object,$attributes,$vote);
81+
7182
if (!\is_int($result) || !(self::VALID_VOTES[$result] ??false)) {
7283
thrownew \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)));
7384
}
7485

86+
$voter =$voterinstanceof TraceableVoter ?$voter->getDecoratedVoter() :$voter;
87+
$vote->voter =$voterinstanceof \Stringable ?$voter :get_debug_type($voter);
88+
$vote->result =$result;
89+
$accessDecision->votes[] =$vote;
90+
7591
yield$result;
7692
}
7793
}
7894

7995
/**
80-
* @return iterable<mixed,VoterInterface>
96+
* @return iterable<VoterInterface>
8197
*/
8298
privatefunctiongetVoters(array$attributes,$object =null):iterable
8399
{

‎src/Symfony/Component/Security/Core/Authorization/AccessDecisionManagerInterface.php

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,9 @@ interface AccessDecisionManagerInterface
2323
/**
2424
* Decides whether the access is possible or not.
2525
*
26-
* @param array $attributes An array of attributes associated with the method being invoked
27-
* @param mixed $object The object to secure
26+
* @param array $attributes An array of attributes associated with the method being invoked
27+
* @param mixed $object The object to secure
28+
* @param AccessDecision $accessDecision Should be used to explain the decision
2829
*/
29-
publicfunctiondecide(TokenInterface$token,array$attributes,mixed$object =null):bool;
30+
publicfunctiondecide(TokenInterface$token,array$attributes,mixed$object =null/* , AccessDecision $accessDecision = new AccessDecision() */):bool;
3031
}

‎src/Symfony/Component/Security/Core/Authorization/AuthorizationChecker.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,14 @@ public function __construct(
3030
) {
3131
}
3232

33-
finalpublicfunctionisGranted(mixed$attribute,mixed$subject =null):bool
33+
finalpublicfunctionisGranted(mixed$attribute,mixed$subject =null,AccessDecision$accessDecision =newAccessDecision()):bool
3434
{
3535
$token =$this->tokenStorage->getToken();
3636

3737
if (!$token || !$token->getUser()) {
3838
$token =newNullToken();
3939
}
4040

41-
return$this->accessDecisionManager->decide($token, [$attribute],$subject);
41+
return$accessDecision->isGranted =$this->accessDecisionManager->decide($token, [$attribute],$subject,$accessDecision);
4242
}
4343
}

‎src/Symfony/Component/Security/Core/Authorization/AuthorizationCheckerInterface.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ interface AuthorizationCheckerInterface
2121
/**
2222
* Checks if the attribute is granted against the current authentication token and optionally supplied subject.
2323
*
24-
* @param mixed $attribute A single attribute to vote on (can be of any type, string and instance of Expression are supported by the core)
24+
* @param mixed $attribute A single attribute to vote on (can be of any type, string and instance of Expression are supported by the core)
25+
* @param AccessDecision $accessDecision Should be used to explain the decision
2526
*/
26-
publicfunctionisGranted(mixed$attribute,mixed$subject =null):bool;
27+
publicfunctionisGranted(mixed$attribute,mixed$subject =null/* , AccessDecision $accessDecision = new AccessDecision() */):bool;
2728
}

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp