Skip to content

Commit ae1f8ab

Browse files
authored
[Symfony 7.3] Replace AuthorizationChecker with AccessDecisionManager in voters (#913)
1 parent 8aa3df7 commit ae1f8ab

File tree

6 files changed

+312
-1
lines changed

6 files changed

+312
-1
lines changed

config/sets/symfony/symfony7/symfony73/symfony73-security-core.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@
44

55
use Rector\Config\RectorConfig;
66
use Rector\Symfony\Symfony73\Rector\Class_\AddVoteArgumentToVoteOnAttributeRector;
7+
use Rector\Symfony\Symfony73\Rector\Class_\AuthorizationCheckerToAccessDecisionManagerInVoterRector;
78

89
return static function (RectorConfig $rectorConfig): void {
9-
$rectorConfig->rules([AddVoteArgumentToVoteOnAttributeRector::class]);
10+
$rectorConfig->rules([
11+
AddVoteArgumentToVoteOnAttributeRector::class,
12+
AuthorizationCheckerToAccessDecisionManagerInVoterRector::class,
13+
]);
1014
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Rector\Symfony\Tests\Symfony73\Rector\Class_\AuthorizationCheckerToAccessDecisionManagerInVoterRector;
6+
7+
use Iterator;
8+
use PHPUnit\Framework\Attributes\DataProvider;
9+
use Rector\Testing\PHPUnit\AbstractRectorTestCase;
10+
11+
final class AuthorizationCheckerToAccessDecisionManagerInVoterRectorTest extends AbstractRectorTestCase
12+
{
13+
#[DataProvider('provideData')]
14+
public function test(string $filePath): void
15+
{
16+
$this->doTestFile($filePath);
17+
}
18+
19+
public static function provideData(): Iterator
20+
{
21+
return self::yieldFilesFromDirectory(__DIR__ . '/Fixture');
22+
}
23+
24+
public function provideConfigFilePath(): string
25+
{
26+
return __DIR__ . '/config/configured_rule.php';
27+
}
28+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<?php
2+
3+
namespace Rector\Symfony\Tests\Symfony73\Rector\Class_\AuthorizationCheckerToAccessDecisionManagerInVoterRector\Fixture;
4+
5+
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
6+
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
7+
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
8+
9+
final class AuthorizationCheckerVoter extends Voter
10+
{
11+
public function __construct(
12+
private AuthorizationCheckerInterface $authorizationChecker
13+
) {}
14+
15+
protected function supports(string $attribute, mixed $subject): bool
16+
{
17+
return true;
18+
}
19+
20+
protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token): bool
21+
{
22+
return $this->authorizationChecker->isGranted('ROLE_ADMIN');
23+
}
24+
}
25+
-----
26+
<?php
27+
28+
namespace Rector\Symfony\Tests\Symfony73\Rector\Class_\AuthorizationCheckerToAccessDecisionManagerInVoterRector\Fixture;
29+
30+
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
31+
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
32+
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
33+
34+
final class AuthorizationCheckerVoter extends Voter
35+
{
36+
public function __construct(
37+
private \Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface $accessDecisionManager
38+
) {}
39+
40+
protected function supports(string $attribute, mixed $subject): bool
41+
{
42+
return true;
43+
}
44+
45+
protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token): bool
46+
{
47+
return $this->accessDecisionManager->decide($token, ['ROLE_ADMIN']);
48+
}
49+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use Rector\Config\RectorConfig;
6+
use Rector\Symfony\Symfony73\Rector\Class_\AuthorizationCheckerToAccessDecisionManagerInVoterRector;
7+
8+
return RectorConfig::configure()
9+
->withRules([
10+
AuthorizationCheckerToAccessDecisionManagerInVoterRector::class,
11+
]);
Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Rector\Symfony\Symfony73\Rector\Class_;
6+
7+
use PhpParser\Node;
8+
use PhpParser\Node\Arg;
9+
use PhpParser\Node\ArrayItem;
10+
use PhpParser\Node\Expr\Array_;
11+
use PhpParser\Node\Expr\MethodCall;
12+
use PhpParser\Node\Expr\PropertyFetch;
13+
use PhpParser\Node\Expr\Variable;
14+
use PhpParser\Node\Identifier;
15+
use PhpParser\Node\Name\FullyQualified;
16+
use PhpParser\Node\Stmt\Class_;
17+
use PhpParser\Node\Stmt\ClassMethod;
18+
use PhpParser\Node\Stmt\Function_;
19+
use PhpParser\NodeVisitor;
20+
use PHPStan\Type\ObjectType;
21+
use Rector\Rector\AbstractRector;
22+
use Rector\Symfony\Enum\SymfonyClass;
23+
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
24+
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
25+
26+
/**
27+
* @see \Rector\Symfony\Tests\Symfony73\Rector\Class_\AuthorizationCheckerToAccessDecisionManagerInVoterRector\AuthorizationCheckerToAccessDecisionManagerInVoterRectorTest
28+
*/
29+
final class AuthorizationCheckerToAccessDecisionManagerInVoterRector extends AbstractRector
30+
{
31+
private const string AUTHORIZATION_CHECKER_PROPERTY = 'authorizationChecker';
32+
private const string ACCESS_DECISION_MANAGER_PROPERTY = 'accessDecisionManager';
33+
34+
public function getRuleDefinition(): RuleDefinition
35+
{
36+
return new RuleDefinition(
37+
'Replaces AuthorizationCheckerInterface with AccessDecisionManagerInterface inside Symfony Voters',
38+
[
39+
new CodeSample(
40+
<<<'CODE_SAMPLE'
41+
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
42+
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
43+
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
44+
45+
final class AuthorizationCheckerVoter extends Voter
46+
{
47+
public function __construct(
48+
private AuthorizationCheckerInterface $authorizationChecker
49+
) {}
50+
51+
protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token): bool
52+
{
53+
return $this->authorizationChecker->isGranted('ROLE_ADMIN');
54+
}
55+
}
56+
CODE_SAMPLE
57+
,
58+
<<<'CODE_SAMPLE'
59+
use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface;
60+
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
61+
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
62+
63+
final class AuthorizationCheckerVoter extends Voter
64+
{
65+
public function __construct(
66+
private AccessDecisionManagerInterface $accessDecisionManager
67+
) {}
68+
69+
protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token): bool
70+
{
71+
return $this->accessDecisionManager->decide($token, ['ROLE_ADMIN']);
72+
}
73+
}
74+
CODE_SAMPLE
75+
),
76+
]
77+
);
78+
}
79+
80+
/**
81+
* @return array<class-string<Node>>
82+
*/
83+
public function getNodeTypes(): array
84+
{
85+
return [Class_::class];
86+
}
87+
88+
/**
89+
* @param Class_ $node
90+
*/
91+
public function refactor(Node $node): ?Node
92+
{
93+
if ($node->extends === null || ! $this->isName($node->extends, SymfonyClass::VOTER_CLASS)) {
94+
return null;
95+
}
96+
97+
$hasChanged = false;
98+
$renamedProperties = [];
99+
100+
$authorizationCheckerType = new ObjectType(
101+
SymfonyClass::AUTHORIZATION_CHECKER
102+
);
103+
104+
// 1) Regular properties
105+
foreach ($node->getProperties() as $property) {
106+
if (! $this->isObjectType($property, $authorizationCheckerType)) {
107+
continue;
108+
}
109+
110+
$property->type = new FullyQualified(
111+
SymfonyClass::ACCESS_DECISION_MANAGER_INTERFACE
112+
);
113+
114+
foreach ($property->props as $prop) {
115+
if ($this->getName($prop) === self::AUTHORIZATION_CHECKER_PROPERTY) {
116+
$prop->name = new Identifier(self::ACCESS_DECISION_MANAGER_PROPERTY);
117+
$renamedProperties[self::AUTHORIZATION_CHECKER_PROPERTY]
118+
= self::ACCESS_DECISION_MANAGER_PROPERTY;
119+
}
120+
}
121+
122+
$hasChanged = true;
123+
}
124+
125+
// 2) Promoted properties (constructor)
126+
$constructor = $node->getMethod('__construct');
127+
if ($constructor instanceof ClassMethod) {
128+
foreach ($constructor->params as $param) {
129+
if (
130+
$param->type === null
131+
|| ! $this->isName($param->type, SymfonyClass::AUTHORIZATION_CHECKER)
132+
) {
133+
continue;
134+
}
135+
136+
$param->type = new FullyQualified(
137+
SymfonyClass::ACCESS_DECISION_MANAGER_INTERFACE
138+
);
139+
140+
if (
141+
$param->var instanceof Variable
142+
&& $this->getName($param->var) === self::AUTHORIZATION_CHECKER_PROPERTY
143+
) {
144+
$param->var->name = self::ACCESS_DECISION_MANAGER_PROPERTY;
145+
$renamedProperties[self::AUTHORIZATION_CHECKER_PROPERTY]
146+
= self::ACCESS_DECISION_MANAGER_PROPERTY;
147+
}
148+
149+
$hasChanged = true;
150+
}
151+
}
152+
153+
// 3) Replace isGranted() with decide()
154+
$voteMethod = $node->getMethod('voteOnAttribute');
155+
if ($voteMethod instanceof ClassMethod) {
156+
$this->traverseNodesWithCallable(
157+
$voteMethod,
158+
function (Node $node) use (&$hasChanged, $voteMethod, $renamedProperties) {
159+
if ($node instanceof Class_ || $node instanceof Function_) {
160+
return NodeVisitor::DONT_TRAVERSE_CURRENT_AND_CHILDREN;
161+
}
162+
163+
if (! $node instanceof MethodCall) {
164+
return null;
165+
}
166+
167+
if (! $this->isObjectType(
168+
$node->var,
169+
new ObjectType(SymfonyClass::AUTHORIZATION_CHECKER)
170+
)) {
171+
return null;
172+
}
173+
174+
if (! $node->var instanceof PropertyFetch) {
175+
return null;
176+
}
177+
178+
if (! $this->isName($node->name, 'isGranted')) {
179+
return null;
180+
}
181+
182+
$propertyName = $this->getName($node->var->name);
183+
if ($propertyName === null || ! isset($renamedProperties[$propertyName])) {
184+
return null;
185+
}
186+
187+
$node->var->name = new Identifier($renamedProperties[$propertyName]);
188+
$node->name = new Identifier('decide');
189+
190+
$tokenVariable = $voteMethod->params[2]->var ?? null;
191+
if (! $tokenVariable instanceof Variable) {
192+
return null;
193+
}
194+
195+
$attributeArg = $node->args[0] ?? null;
196+
if (! $attributeArg instanceof Arg) {
197+
return null;
198+
}
199+
200+
$attributeExpr = $attributeArg->value;
201+
202+
$node->args = [
203+
new Arg($tokenVariable),
204+
new Arg(new Array_([
205+
new ArrayItem($attributeExpr),
206+
])),
207+
];
208+
209+
$hasChanged = true;
210+
return $node;
211+
}
212+
);
213+
}
214+
215+
return $hasChanged ? $node : null;
216+
}
217+
}

src/Enum/SymfonyClass.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,8 @@ final class SymfonyClass
9090

9191
public const string USER_INTERFACE = 'Symfony\Component\Security\Core\User\UserInterface';
9292

93+
public const string ACCESS_DECISION_MANAGER_INTERFACE = 'Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface';
94+
9395
public const string UUID = 'Symfony\Component\Uid\Uuid';
9496

9597
public const string ROUTE_COLLECTION_BUILDER = 'Symfony\Component\Routing\RouteCollectionBuilder';

0 commit comments

Comments
 (0)