Skip to content

Commit bb860bf

Browse files
[Security] Add ability for voters to explain their vote
1 parent 24db393 commit bb860bf

File tree

2 files changed

+59
-7
lines changed

2 files changed

+59
-7
lines changed

Controller/AbstractController.php

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
3636
use Symfony\Component\Routing\RouterInterface;
3737
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
38+
use Symfony\Component\Security\Core\Authorization\AccessDecision;
3839
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
3940
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
4041
use Symfony\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+
protected function getAccessDecision(mixed $attribute, mixed $subject = null): AccessDecision
210+
{
211+
if (!$this->container->has('security.authorization_checker')) {
212+
throw new \LogicException('The SecurityBundle is not registered in your application. Try running "composer require symfony/security-bundle".');
213+
}
214+
215+
$accessDecision = new AccessDecision();
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
protected function denyAccessUnlessGranted(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

Tests/Controller/AbstractControllerTest.php

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,10 @@
4040
use Symfony\Component\Routing\RouterInterface;
4141
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;
4242
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
43+
use Symfony\Component\Security\Core\Authorization\AccessDecision;
4344
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
45+
use Symfony\Component\Security\Core\Authorization\Voter\Vote;
46+
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
4447
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
4548
use Symfony\Component\Security\Core\User\InMemoryUser;
4649
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
@@ -352,7 +355,19 @@ public function testIsGranted()
352355
public function testdenyAccessUnlessGranted()
353356
{
354357
$authorizationChecker = $this->createMock(AuthorizationCheckerInterface::class);
355-
$authorizationChecker->expects($this->once())->method('isGranted')->willReturn(false);
358+
$authorizationChecker
359+
->expects($this->once())
360+
->method('isGranted')
361+
->willReturnCallback(function ($attribute, $subject, ?AccessDecision $accessDecision = null) {
362+
if (class_exists(AccessDecision::class)) {
363+
$this->assertInstanceOf(AccessDecision::class, $accessDecision);
364+
$accessDecision->votes[] = $vote = new Vote();
365+
$vote->result = VoterInterface::ACCESS_DENIED;
366+
$vote->reasons[] = 'Why should I.';
367+
}
368+
369+
return false;
370+
});
356371

357372
$container = new Container();
358373
$container->set('security.authorization_checker', $authorizationChecker);
@@ -361,8 +376,17 @@ public function testdenyAccessUnlessGranted()
361376
$controller->setContainer($container);
362377

363378
$this->expectException(AccessDeniedException::class);
379+
$this->expectExceptionMessage('Access Denied.'.(class_exists(AccessDecision::class) ? ' Why should I.' : ''));
364380

365-
$controller->denyAccessUnlessGranted('foo');
381+
try {
382+
$controller->denyAccessUnlessGranted('foo');
383+
} catch (AccessDeniedException $e) {
384+
if (class_exists(AccessDecision::class)) {
385+
$this->assertFalse($e->getAccessDecision()->isGranted);
386+
}
387+
388+
throw $e;
389+
}
366390
}
367391

368392
/**

0 commit comments

Comments
 (0)