Skip to content

Commit 3728d69

Browse files
[Security] Improve DX of recent additions
1 parent 7c7d2de commit 3728d69

7 files changed

+191
-137
lines changed

Attribute/IsGranted.php

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,6 @@
1313

1414
use Symfony\Component\ExpressionLanguage\Expression;
1515
use Symfony\Component\HttpFoundation\Request;
16-
use Symfony\Component\Security\Core\Authentication\AuthenticationTrustResolverInterface;
17-
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
18-
use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface;
1916

2017
/**
2118
* Checks if user has permission to access to some resource using security roles and voters.
@@ -28,8 +25,8 @@
2825
final class IsGranted
2926
{
3027
/**
31-
* @param string|Expression|(\Closure(TokenInterface $token, mixed $subject, AccessDecisionManagerInterface $accessDecisionManager, AuthenticationTrustResolverInterface $trustResolver): bool) $attribute The attribute that will be checked against a given authentication token and optional subject
32-
* @param array|string|Expression|(\Closure(array<string, mixed>, Request): mixed)|null $subject An optional subject - e.g. the current object being voted on
28+
* @param string|Expression|\Closure(IsGrantedContext, mixed $subject):bool $attribute The attribute that will be checked against a given authentication token and optional subject
29+
* @param array|string|Expression|\Closure(array<string,mixed>, Request):mixed|null $subject An optional subject - e.g. the current object being voted on
3330
* @param string|null $message A custom message when access is not granted
3431
* @param int|null $statusCode If set, will throw HttpKernel's HttpException with the given $statusCode; if null, Security\Core's AccessDeniedException will be used
3532
* @param int|null $exceptionCode If set, will add the exception code to thrown exception

Attribute/IsGrantedContext.php

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
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+
namespace Symfony\Component\Security\Http\Attribute;
13+
14+
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
15+
use Symfony\Component\Security\Core\Authorization\AccessDecision;
16+
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
17+
use Symfony\Component\Security\Core\Authorization\Voter\AuthenticatedVoter;
18+
use Symfony\Component\Security\Core\User\UserInterface;
19+
20+
readonly class IsGrantedContext implements AuthorizationCheckerInterface
21+
{
22+
public function __construct(
23+
public TokenInterface $token,
24+
public ?UserInterface $user,
25+
private AuthorizationCheckerInterface $authorizationChecker,
26+
) {
27+
}
28+
29+
public function isGranted(mixed $attribute, mixed $subject = null, ?AccessDecision $accessDecision = null): bool
30+
{
31+
return $this->authorizationChecker->isGranted($attribute, $subject, $accessDecision);
32+
}
33+
34+
public function isAuthenticated(): bool
35+
{
36+
return $this->authorizationChecker->isGranted(AuthenticatedVoter::IS_AUTHENTICATED);
37+
}
38+
39+
public function isAuthenticatedFully(): bool
40+
{
41+
return $this->authorizationChecker->isGranted(AuthenticatedVoter::IS_AUTHENTICATED_FULLY);
42+
}
43+
44+
public function isImpersonator(): bool
45+
{
46+
return $this->authorizationChecker->isGranted(AuthenticatedVoter::IS_IMPERSONATOR);
47+
}
48+
}

EventListener/IsGrantedAttributeListener.php

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,6 @@ public function onKernelControllerArguments(ControllerArgumentsEvent $event): vo
5555
foreach ($subjectRef as $refKey => $ref) {
5656
$subject[\is_string($refKey) ? $refKey : (string) $ref] = $this->getIsGrantedSubject($ref, $request, $arguments);
5757
}
58-
} elseif ($subjectRef instanceof \Closure) {
59-
$subject = $subjectRef($arguments, $request);
6058
} else {
6159
$subject = $this->getIsGrantedSubject($subjectRef, $request, $arguments);
6260
}
@@ -85,8 +83,12 @@ public static function getSubscribedEvents(): array
8583
return [KernelEvents::CONTROLLER_ARGUMENTS => ['onKernelControllerArguments', 20]];
8684
}
8785

88-
private function getIsGrantedSubject(string|Expression $subjectRef, Request $request, array $arguments): mixed
86+
private function getIsGrantedSubject(string|Expression|\Closure $subjectRef, Request $request, array $arguments): mixed
8987
{
88+
if ($subjectRef instanceof \Closure) {
89+
return $subjectRef($arguments, $request);
90+
}
91+
9092
if ($subjectRef instanceof Expression) {
9193
$this->expressionLanguage ??= new ExpressionLanguage();
9294

Tests/EventListener/IsGrantedAttributeWithCallableListenerTest.php renamed to Tests/EventListener/IsGrantedAttributeWithClosureListenerTest.php

Lines changed: 32 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,20 @@
1616
use Symfony\Component\HttpKernel\Event\ControllerArgumentsEvent;
1717
use Symfony\Component\HttpKernel\Exception\HttpException;
1818
use Symfony\Component\HttpKernel\HttpKernelInterface;
19+
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;
20+
use Symfony\Component\Security\Core\Authorization\AccessDecisionManager;
21+
use Symfony\Component\Security\Core\Authorization\AuthorizationChecker;
1922
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
23+
use Symfony\Component\Security\Core\Authorization\Voter\ClosureVoter;
2024
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
2125
use Symfony\Component\Security\Http\EventListener\IsGrantedAttributeListener;
22-
use Symfony\Component\Security\Http\Tests\Fixtures\IsGrantedAttributeMethodsWithCallableController;
23-
use Symfony\Component\Security\Http\Tests\Fixtures\IsGrantedAttributeWithCallableController;
26+
use Symfony\Component\Security\Http\Tests\Fixtures\IsGrantedAttributeMethodsWithClosureController;
27+
use Symfony\Component\Security\Http\Tests\Fixtures\IsGrantedAttributeWithClosureController;
2428

2529
/**
2630
* @requires PHP 8.5
2731
*/
28-
class IsGrantedAttributeWithCallableListenerTest extends TestCase
32+
class IsGrantedAttributeWithClosureListenerTest extends TestCase
2933
{
3034
public function testAttribute()
3135
{
@@ -36,7 +40,7 @@ public function testAttribute()
3640

3741
$event = new ControllerArgumentsEvent(
3842
$this->createMock(HttpKernelInterface::class),
39-
[new IsGrantedAttributeWithCallableController(), 'foo'],
43+
[new IsGrantedAttributeWithClosureController(), 'foo'],
4044
[],
4145
new Request(),
4246
null
@@ -52,7 +56,7 @@ public function testAttribute()
5256

5357
$event = new ControllerArgumentsEvent(
5458
$this->createMock(HttpKernelInterface::class),
55-
[new IsGrantedAttributeWithCallableController(), 'bar'],
59+
[new IsGrantedAttributeWithClosureController(), 'bar'],
5660
[],
5761
new Request(),
5862
null
@@ -70,7 +74,7 @@ public function testNothingHappensWithNoConfig()
7074

7175
$event = new ControllerArgumentsEvent(
7276
$this->createMock(HttpKernelInterface::class),
73-
[new IsGrantedAttributeMethodsWithCallableController(), 'noAttribute'],
77+
[new IsGrantedAttributeMethodsWithClosureController(), 'noAttribute'],
7478
[],
7579
new Request(),
7680
null
@@ -90,7 +94,7 @@ public function testIsGrantedCalledCorrectly()
9094

9195
$event = new ControllerArgumentsEvent(
9296
$this->createMock(HttpKernelInterface::class),
93-
[new IsGrantedAttributeMethodsWithCallableController(), 'admin'],
97+
[new IsGrantedAttributeMethodsWithClosureController(), 'admin'],
9498
[],
9599
new Request(),
96100
null
@@ -111,7 +115,7 @@ public function testIsGrantedSubjectFromArguments()
111115

112116
$event = new ControllerArgumentsEvent(
113117
$this->createMock(HttpKernelInterface::class),
114-
[new IsGrantedAttributeMethodsWithCallableController(), 'withSubject'],
118+
[new IsGrantedAttributeMethodsWithClosureController(), 'withSubject'],
115119
['arg1Value', 'arg2Value'],
116120
new Request(),
117121
null
@@ -136,7 +140,7 @@ public function testIsGrantedSubjectFromArgumentsWithArray()
136140

137141
$event = new ControllerArgumentsEvent(
138142
$this->createMock(HttpKernelInterface::class),
139-
[new IsGrantedAttributeMethodsWithCallableController(), 'withSubjectArray'],
143+
[new IsGrantedAttributeMethodsWithClosureController(), 'withSubjectArray'],
140144
['arg1Value', 'arg2Value'],
141145
new Request(),
142146
null
@@ -157,7 +161,7 @@ public function testIsGrantedNullSubjectFromArguments()
157161

158162
$event = new ControllerArgumentsEvent(
159163
$this->createMock(HttpKernelInterface::class),
160-
[new IsGrantedAttributeMethodsWithCallableController(), 'withSubject'],
164+
[new IsGrantedAttributeMethodsWithClosureController(), 'withSubject'],
161165
['arg1Value', null],
162166
new Request(),
163167
null
@@ -180,7 +184,7 @@ public function testIsGrantedArrayWithNullValueSubjectFromArguments()
180184

181185
$event = new ControllerArgumentsEvent(
182186
$this->createMock(HttpKernelInterface::class),
183-
[new IsGrantedAttributeMethodsWithCallableController(), 'withSubjectArray'],
187+
[new IsGrantedAttributeMethodsWithClosureController(), 'withSubjectArray'],
184188
['arg1Value', null],
185189
new Request(),
186190
null
@@ -196,7 +200,7 @@ public function testExceptionWhenMissingSubjectAttribute()
196200

197201
$event = new ControllerArgumentsEvent(
198202
$this->createMock(HttpKernelInterface::class),
199-
[new IsGrantedAttributeMethodsWithCallableController(), 'withMissingSubject'],
203+
[new IsGrantedAttributeMethodsWithClosureController(), 'withMissingSubject'],
200204
[],
201205
new Request(),
202206
null
@@ -214,18 +218,17 @@ public function testExceptionWhenMissingSubjectAttribute()
214218
*/
215219
public function testAccessDeniedMessages(string|array|null $subject, string $method, int $numOfArguments, string $expectedMessage)
216220
{
217-
$authChecker = $this->createMock(AuthorizationCheckerInterface::class);
218-
$authChecker->expects($this->any())
219-
->method('isGranted')
220-
->willReturn(false);
221+
$authChecker = new AuthorizationChecker(new TokenStorage(), new AccessDecisionManager((function () use (&$authChecker) {
222+
yield new ClosureVoter($authChecker);
223+
})()));
221224

222225
// avoid the error of the subject not being found in the request attributes
223226
$arguments = array_fill(0, $numOfArguments, 'bar');
224227
$listener = new IsGrantedAttributeListener($authChecker);
225228

226229
$event = new ControllerArgumentsEvent(
227230
$this->createMock(HttpKernelInterface::class),
228-
[new IsGrantedAttributeMethodsWithCallableController(), $method],
231+
[new IsGrantedAttributeMethodsWithClosureController(), $method],
229232
$arguments,
230233
new Request(),
231234
null
@@ -236,7 +239,7 @@ public function testAccessDeniedMessages(string|array|null $subject, string $met
236239
$this->fail();
237240
} catch (AccessDeniedException $e) {
238241
$this->assertSame($expectedMessage, $e->getMessage());
239-
$this->assertIsCallable($e->getAttributes()[0]);
242+
$this->assertInstanceOf(\Closure::class, $e->getAttributes()[0]);
240243
if (null !== $subject) {
241244
$this->assertSame($subject, $e->getSubject());
242245
} else {
@@ -247,11 +250,11 @@ public function testAccessDeniedMessages(string|array|null $subject, string $met
247250

248251
public static function getAccessDeniedMessageTests()
249252
{
250-
yield [null, 'admin', 0, 'Access Denied by #[IsGranted({closure:Symfony\Component\Security\Http\Tests\Fixtures\IsGrantedAttributeMethodsWithCallableController::admin():23})] on controller'];
251-
yield ['bar', 'withSubject', 2, 'Access Denied by #[IsGranted({closure:Symfony\Component\Security\Http\Tests\Fixtures\IsGrantedAttributeMethodsWithCallableController::withSubject():30}, "arg2Name")] on controller'];
252-
yield [['arg1Name' => 'bar', 'arg2Name' => 'bar'], 'withSubjectArray', 2, 'Access Denied by #[IsGranted({closure:Symfony\Component\Security\Http\Tests\Fixtures\IsGrantedAttributeMethodsWithCallableController::withSubjectArray():37}, ["arg1Name", "arg2Name"])] on controller'];
253-
yield ['bar', 'withCallableAsSubject', 1, 'Access Denied by #[IsGranted({closure:Symfony\Component\Security\Http\Tests\Fixtures\IsGrantedAttributeMethodsWithCallableController::withCallableAsSubject():73}, {closure:Symfony\Component\Security\Http\Tests\Fixtures\IsGrantedAttributeMethodsWithCallableController::withCallableAsSubject():76})] on controller'];
254-
yield [['author' => 'bar', 'alias' => 'bar'], 'withNestArgsInSubject', 2, 'Access Denied by #[IsGranted({closure:Symfony\Component\Security\Http\Tests\Fixtures\IsGrantedAttributeMethodsWithCallableController::withNestArgsInSubject():84}, {closure:Symfony\Component\Security\Http\Tests\Fixtures\IsGrantedAttributeMethodsWithCallableController::withNestArgsInSubject():86})] on controller'];
253+
yield [null, 'admin', 0, 'Access Denied. Closure {closure:Symfony\Component\Security\Http\Tests\Fixtures\IsGrantedAttributeMethodsWithClosureController::admin():23} returned false.'];
254+
yield ['bar', 'withSubject', 2, 'Access Denied. Closure {closure:Symfony\Component\Security\Http\Tests\Fixtures\IsGrantedAttributeMethodsWithClosureController::withSubject():30} returned false.'];
255+
yield [['arg1Name' => 'bar', 'arg2Name' => 'bar'], 'withSubjectArray', 2, 'Access Denied. Closure {closure:Symfony\Component\Security\Http\Tests\Fixtures\IsGrantedAttributeMethodsWithClosureController::withSubjectArray():37} returned false.'];
256+
yield ['bar', 'withClosureAsSubject', 1, 'Access Denied. Closure {closure:Symfony\Component\Security\Http\Tests\Fixtures\IsGrantedAttributeMethodsWithClosureController::withClosureAsSubject():73} returned false.'];
257+
yield [['author' => 'bar', 'alias' => 'bar'], 'withNestArgsInSubject', 2, 'Access Denied. Closure {closure:Symfony\Component\Security\Http\Tests\Fixtures\IsGrantedAttributeMethodsWithClosureController::withNestArgsInSubject():85} returned false.'];
255258
}
256259

257260
public function testNotFoundHttpException()
@@ -263,7 +266,7 @@ public function testNotFoundHttpException()
263266

264267
$event = new ControllerArgumentsEvent(
265268
$this->createMock(HttpKernelInterface::class),
266-
[new IsGrantedAttributeMethodsWithCallableController(), 'notFound'],
269+
[new IsGrantedAttributeMethodsWithClosureController(), 'notFound'],
267270
[],
268271
new Request(),
269272
null
@@ -277,7 +280,7 @@ public function testNotFoundHttpException()
277280
$listener->onKernelControllerArguments($event);
278281
}
279282

280-
public function testIsGrantedWithCallableAsSubject()
283+
public function testIsGrantedWithClosureAsSubject()
281284
{
282285
$request = new Request();
283286

@@ -289,7 +292,7 @@ public function testIsGrantedWithCallableAsSubject()
289292

290293
$event = new ControllerArgumentsEvent(
291294
$this->createMock(HttpKernelInterface::class),
292-
[new IsGrantedAttributeMethodsWithCallableController(), 'withCallableAsSubject'],
295+
[new IsGrantedAttributeMethodsWithClosureController(), 'withClosureAsSubject'],
293296
['postVal'],
294297
$request,
295298
null
@@ -311,7 +314,7 @@ public function testIsGrantedWithNestedExpressionInSubject()
311314

312315
$event = new ControllerArgumentsEvent(
313316
$this->createMock(HttpKernelInterface::class),
314-
[new IsGrantedAttributeMethodsWithCallableController(), 'withNestArgsInSubject'],
317+
[new IsGrantedAttributeMethodsWithClosureController(), 'withNestArgsInSubject'],
315318
['postVal', 'bar'],
316319
$request,
317320
null
@@ -330,7 +333,7 @@ public function testHttpExceptionWithExceptionCode()
330333

331334
$event = new ControllerArgumentsEvent(
332335
$this->createMock(HttpKernelInterface::class),
333-
[new IsGrantedAttributeMethodsWithCallableController(), 'exceptionCodeInHttpException'],
336+
[new IsGrantedAttributeMethodsWithClosureController(), 'exceptionCodeInHttpException'],
334337
[],
335338
new Request(),
336339
null
@@ -354,7 +357,7 @@ public function testAccessDeniedExceptionWithExceptionCode()
354357

355358
$event = new ControllerArgumentsEvent(
356359
$this->createMock(HttpKernelInterface::class),
357-
[new IsGrantedAttributeMethodsWithCallableController(), 'exceptionCodeInAccessDeniedException'],
360+
[new IsGrantedAttributeMethodsWithClosureController(), 'exceptionCodeInAccessDeniedException'],
358361
[],
359362
new Request(),
360363
null

Tests/Fixtures/IsGrantedAttributeMethodsWithCallableController.php

Lines changed: 0 additions & 95 deletions
This file was deleted.

0 commit comments

Comments
 (0)