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

Commita28dcfd

Browse files
[DX][Security] Allow using a callable with#[IsGranted]
1 parent8c841e3 commita28dcfd

File tree

12 files changed

+789
-16
lines changed

12 files changed

+789
-16
lines changed

‎src/Symfony/Bundle/SecurityBundle/Resources/config/security.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
useSymfony\Component\Security\Core\Authorization\UserAuthorizationChecker;
3535
useSymfony\Component\Security\Core\Authorization\UserAuthorizationCheckerInterface;
3636
useSymfony\Component\Security\Core\Authorization\Voter\AuthenticatedVoter;
37+
useSymfony\Component\Security\Core\Authorization\Voter\CallableVoter;
3738
useSymfony\Component\Security\Core\Authorization\Voter\ExpressionVoter;
3839
useSymfony\Component\Security\Core\Authorization\Voter\RoleHierarchyVoter;
3940
useSymfony\Component\Security\Core\Authorization\Voter\RoleVoter;
@@ -171,6 +172,14 @@
171172
])
172173
->tag('security.voter', ['priority' =>245])
173174

175+
->set('security.access.callable_voter', CallableVoter::class)
176+
->args([
177+
service('security.authentication.trust_resolver'),
178+
service('security.authorization_checker'),
179+
service('security.role_hierarchy')->nullOnInvalid(),
180+
])
181+
->tag('security.voter', ['priority' =>245])
182+
174183
->set('security.impersonate_url_generator', ImpersonateUrlGenerator::class)
175184
->args([
176185
service('request_stack'),

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ 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 andinstance of Expression are supported by the core)
24+
* @param mixed $attribute A single attribute to vote on (can be of any type, string,instance of Expression and Closures are supported by the core)
2525
*/
2626
publicfunctionisGranted(mixed$attribute,mixed$subject =null):bool;
2727
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
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\Voter;
13+
14+
useSymfony\Component\Security\Core\Authentication\AuthenticationTrustResolverInterface;
15+
useSymfony\Component\Security\Core\Authentication\Token\TokenInterface;
16+
useSymfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
17+
18+
/**
19+
* @author Alexandre Daubois <alex.daubois@gmail.com>
20+
*/
21+
finalclass CallableVoterimplements CacheableVoterInterface
22+
{
23+
publicfunction__construct(
24+
privateAuthenticationTrustResolverInterface$trustResolver,
25+
privateAuthorizationCheckerInterface$authChecker,
26+
) {
27+
}
28+
29+
publicfunctionsupportsAttribute(string$attribute):bool
30+
{
31+
returnfalse;
32+
}
33+
34+
publicfunctionsupportsType(string$subjectType):bool
35+
{
36+
returntrue;
37+
}
38+
39+
publicfunctionvote(TokenInterface$token,mixed$subject,array$attributes):int
40+
{
41+
$result = VoterInterface::ACCESS_ABSTAIN;
42+
foreach ($attributesas$attribute) {
43+
if (!$attributeinstanceof \Closure) {
44+
continue;
45+
}
46+
47+
$result = VoterInterface::ACCESS_DENIED;
48+
if ($attribute($token,$subject,$this->trustResolver,$this->authChecker)) {
49+
return VoterInterface::ACCESS_GRANTED;
50+
}
51+
}
52+
53+
return$result;
54+
}
55+
}

‎src/Symfony/Component/Security/Core/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ CHANGELOG
77
* Add`UserAuthorizationChecker::userIsGranted()` to test user authorization without relying on the session.
88
For example, users not currently logged in, or while processing a message from a message queue.
99
* Add`OfflineTokenInterface` to mark tokens that do not represent the currently logged-in user
10+
* Add support for voting on callables
1011

1112
7.2
1213
---
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
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\Tests\Authorization\Voter;
13+
14+
usePHPUnit\Framework\TestCase;
15+
useSymfony\Component\Security\Core\Authentication\AuthenticationTrustResolverInterface;
16+
useSymfony\Component\Security\Core\Authentication\Token\TokenInterface;
17+
useSymfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
18+
useSymfony\Component\Security\Core\Authorization\Voter\CallableVoter;
19+
useSymfony\Component\Security\Core\Authorization\Voter\VoterInterface;
20+
useSymfony\Component\Security\Core\User\UserInterface;
21+
22+
class CallableVoterTestextends TestCase
23+
{
24+
publicfunctiontestEmptyAttributeAbstains()
25+
{
26+
$voter =newCallableVoter(
27+
$this->createMock(AuthenticationTrustResolverInterface::class),
28+
$this->createMock(AuthorizationCheckerInterface::class),
29+
);
30+
31+
$this->assertSame(VoterInterface::ACCESS_ABSTAIN,$voter->vote(
32+
$this->createMock(TokenInterface::class),
33+
null,
34+
[])
35+
);
36+
}
37+
38+
publicfunctiontestClosureReturningFalseDeniesAccess()
39+
{
40+
$token =$this->createMock(TokenInterface::class);
41+
$token->method('getRoleNames')->willReturn([]);
42+
$token->method('getUser')->willReturn($this->createMock(UserInterface::class));
43+
44+
$voter =newCallableVoter(
45+
$this->createMock(AuthenticationTrustResolverInterface::class),
46+
$this->createMock(AuthorizationCheckerInterface::class),
47+
);
48+
49+
$this->assertSame(VoterInterface::ACCESS_DENIED,$voter->vote(
50+
$token,
51+
null,
52+
[fn () =>false]
53+
));
54+
}
55+
56+
publicfunctiontestClosureReturningTrueGrantsAccess()
57+
{
58+
$token =$this->createMock(TokenInterface::class);
59+
$token->method('getRoleNames')->willReturn([]);
60+
$token->method('getUser')->willReturn($this->createMock(UserInterface::class));
61+
62+
$voter =newCallableVoter(
63+
$this->createMock(AuthenticationTrustResolverInterface::class),
64+
$this->createMock(AuthorizationCheckerInterface::class),
65+
);
66+
67+
$this->assertSame(VoterInterface::ACCESS_GRANTED,$voter->vote(
68+
$token,
69+
null,
70+
[fn () =>true]
71+
));
72+
}
73+
74+
publicfunctiontestPayloadContent()
75+
{
76+
$token =$this->createMock(TokenInterface::class);
77+
$token->method('getRoleNames')->willReturn(['MY_ROLE','ANOTHER_ROLE']);
78+
$token->method('getUser')->willReturn($this->createMock(UserInterface::class));
79+
80+
$subject =new \stdClass();
81+
82+
$voter =newCallableVoter(
83+
$this->createMock(AuthenticationTrustResolverInterface::class),
84+
$this->createMock(AuthorizationCheckerInterface::class),
85+
);
86+
87+
$voter->vote(
88+
$token,
89+
$subject,
90+
[function ($token,$closureSubject,$trustResolver,$authorizationChecker)use ($subject) {
91+
$this->assertInstanceOf(TokenInterface::class,$token);
92+
$this->assertSame($subject,$closureSubject);
93+
94+
$this->assertInstanceOf(AuthenticationTrustResolverInterface::class,$trustResolver);
95+
$this->assertInstanceOf(AuthorizationCheckerInterface::class,$authorizationChecker);
96+
97+
returntrue;
98+
}]
99+
);
100+
}
101+
}

‎src/Symfony/Component/Security/Http/Attribute/IsGranted.php

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
namespaceSymfony\Component\Security\Http\Attribute;
1313

1414
useSymfony\Component\ExpressionLanguage\Expression;
15+
useSymfony\Component\HttpFoundation\Request;
16+
useSymfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
1517

1618
/**
1719
* Checks if user has permission to access to some resource using security roles and voters.
@@ -24,18 +26,37 @@
2426
finalclass IsGranted
2527
{
2628
/**
27-
* @param string|Expression $attribute The attribute that will be checked against a given authentication token and optional subject
28-
* @param array|string|Expression|null $subject An optional subject - e.g. the current object being voted on
29-
* @param string|null $message A custom message when access is not granted
30-
* @param int|null $statusCode If set, will throw HttpKernel's HttpException with the given $statusCode; if null, Security\Core's AccessDeniedException will be used
31-
* @param int|null $exceptionCode If set, will add the exception code to thrown exception
29+
* @param string|Expression|null $attribute The attribute that will be checked against a given authentication token and optional subject, or a callable that will be called to determine access
30+
* @param array|string|Expression|\Closure(Request $request, array $arguments): mixed|null $subject An optional subject - e.g. the current object being voted on
31+
* @param string|null $message A custom message when access is not granted
32+
* @param int|null $statusCode If set, will throw HttpKernel's HttpException with the given $statusCode; if null, Security\Core's AccessDeniedException will be used
33+
* @param int|null $exceptionCode If set, will add the exception code to thrown exception
34+
* @param \Closure(AuthorizationCheckerInterface, mixed $subject): bool|null $callable A callable that will be called to determine access
3235
*/
3336
publicfunction__construct(
34-
publicstring|Expression$attribute,
35-
publicarray|string|Expression|null$subject =null,
37+
publicstring|Expression|null$attribute =null,
38+
publicarray|string|Expression|\Closure|null$subject =null,
3639
public ?string$message =null,
3740
public ?int$statusCode =null,
3841
public ?int$exceptionCode =null,
42+
public ?\Closure$callable =null,
3943
) {
44+
if (\PHP_VERSION_ID <80500) {
45+
if (null ===$attribute) {
46+
thrownew \LogicException('The "attribute" argument is only optional starting from PHP 8.5.');
47+
}
48+
49+
if (null !==$callable) {
50+
thrownew \LogicException('The "callable" argument is usable only starting from PHP 8.5.');
51+
}
52+
53+
if ($subjectinstanceof \Closure) {
54+
thrownew \LogicException('The "subject" argument can be a callable only starting from PHP 8.5.');
55+
}
56+
}else {
57+
if (!(null ===$attributexornull ===$callable)) {
58+
thrownew \LogicException('Either the "attribute" or "callable" argument must be set.');
59+
}
60+
}
4061
}
4162
}

‎src/Symfony/Component/Security/Http/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
CHANGELOG
22
=========
33

4+
7.3
5+
---
6+
7+
* Add support for closures in`#[IsGranted]`
8+
49
7.2
510
---
611

‎src/Symfony/Component/Security/Http/EventListener/IsGrantedAttributeListener.php

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -54,20 +54,26 @@ public function onKernelControllerArguments(ControllerArgumentsEvent $event): vo
5454
foreach ($subjectRefas$refKey =>$ref) {
5555
$subject[\is_string($refKey) ?$refKey : (string)$ref] =$this->getIsGrantedSubject($ref,$request,$arguments);
5656
}
57+
}elseif ($subjectRefinstanceof \Closure) {
58+
$subject =$subjectRef($request,$arguments);
5759
}else {
5860
$subject =$this->getIsGrantedSubject($subjectRef,$request,$arguments);
5961
}
6062
}
6163

62-
if (!$this->authChecker->isGranted($attribute->attribute,$subject)) {
64+
if (!$this->authChecker->isGranted($attribute->callable ??$attribute->attribute,$subject)) {
6365
$message =$attribute->message ?:\sprintf('Access Denied by #[IsGranted(%s)] on controller',$this->getIsGrantedString($attribute));
6466

6567
if ($statusCode =$attribute->statusCode) {
6668
thrownewHttpException($statusCode,$message, code:$attribute->exceptionCode ??0);
6769
}
6870

6971
$accessDeniedException =newAccessDeniedException($message, code:$attribute->exceptionCode ??403);
70-
$accessDeniedException->setAttributes($attribute->attribute);
72+
73+
if ($attribute->attribute) {
74+
$accessDeniedException->setAttributes($attribute->attribute);
75+
}
76+
7177
$accessDeniedException->setSubject($subject);
7278

7379
throw$accessDeniedException;
@@ -102,14 +108,18 @@ private function getIsGrantedString(IsGranted $isGranted): string
102108
{
103109
$processValue =fn ($value) =>\sprintf($valueinstanceof Expression ?'new Expression("%s")' :'"%s"',$value);
104110

105-
$argsString =$processValue($isGranted->attribute);
111+
$argsString =$isGranted->callable ?'callable' :$processValue($isGranted->attribute);
106112

107113
if (null !==$subject =$isGranted->subject) {
108-
$subject = !\is_array($subject) ?$processValue($subject) :array_map(function ($key,$value)use ($processValue) {
109-
$value =$processValue($value);
110-
111-
return\is_string($key) ?\sprintf('"%s" => %s',$key,$value) :$value;
112-
},array_keys($subject),$subject);
114+
if (\is_callable($subject)) {
115+
$subject ='callable';
116+
}else {
117+
$subject = !\is_array($subject) ?$processValue($subject) :array_map(function ($key,$value)use ($processValue) {
118+
$value =$processValue($value);
119+
120+
return\is_string($key) ?\sprintf('"%s" => %s',$key,$value) :$value;
121+
},array_keys($subject),$subject);
122+
}
113123

114124
$argsString .=','.(!\is_array($subject) ?$subject :'['.implode(',',$subject).']');
115125
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
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\Http\Tests\Attribute;
13+
14+
usePHPUnit\Framework\TestCase;
15+
useSymfony\Component\Security\Http\Attribute\IsGranted;
16+
17+
class IsGrantedTestextends TestCase
18+
{
19+
/**
20+
* @requires PHP < 8.5
21+
*/
22+
publicfunctiontestNullAttributePriorToPhp85()
23+
{
24+
$this->expectException(\LogicException::class);
25+
$this->expectExceptionMessage('The "attribute" argument is only optional starting from PHP 8.5.');
26+
27+
newIsGranted();
28+
}
29+
30+
/**
31+
* @requires PHP < 8.5
32+
*/
33+
publicfunctiontestNotNullCallablePriorToPhp85()
34+
{
35+
$this->expectException(\LogicException::class);
36+
$this->expectExceptionMessage('The "callable" argument is usable only starting from PHP 8.5.');
37+
38+
newIsGranted('attribute', callable:staticfunction () {});
39+
}
40+
41+
/**
42+
* @requires PHP < 8.5
43+
*/
44+
publicfunctiontestCallableSubjectPriorToPhp85()
45+
{
46+
$this->expectException(\LogicException::class);
47+
$this->expectExceptionMessage('The "subject" argument can be a callable only starting from PHP 8.5.');
48+
49+
newIsGranted('attribute', subject:staticfunction () {});
50+
}
51+
52+
/**
53+
* @requires PHP 8.5
54+
*/
55+
publicfunctiontestAttributeAndCallableAreNull()
56+
{
57+
$this->expectException(\LogicException::class);
58+
$this->expectExceptionMessage('Either the "attribute" or "callable" argument must be set.');
59+
60+
newIsGranted();
61+
}
62+
63+
/**
64+
* @requires PHP 8.5
65+
*/
66+
publicfunctiontestAttributeAndCallableAreNotNull()
67+
{
68+
$this->expectException(\LogicException::class);
69+
$this->expectExceptionMessage('Either the "attribute" or "callable" argument must be set.');
70+
71+
newIsGranted('attribute', callable:staticfunction () {});
72+
}
73+
}

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp