diff --git a/config/sets/symfony/symfony7/symfony73/symfony73-security-core.php b/config/sets/symfony/symfony7/symfony73/symfony73-security-core.php index 35092ed03..71f2857a7 100644 --- a/config/sets/symfony/symfony7/symfony73/symfony73-security-core.php +++ b/config/sets/symfony/symfony7/symfony73/symfony73-security-core.php @@ -4,7 +4,11 @@ use Rector\Config\RectorConfig; use Rector\Symfony\Symfony73\Rector\Class_\AddVoteArgumentToVoteOnAttributeRector; +use Rector\Symfony\Symfony73\Rector\Class_\AuthorizationCheckerToAccessDecisionManagerInVoterRector; return static function (RectorConfig $rectorConfig): void { - $rectorConfig->rules([AddVoteArgumentToVoteOnAttributeRector::class]); + $rectorConfig->rules([ + AddVoteArgumentToVoteOnAttributeRector::class, + AuthorizationCheckerToAccessDecisionManagerInVoterRector::class, + ]); }; diff --git a/rules-tests/Symfony73/Rector/Class_/AuthorizationCheckerToAccessDecisionManagerInVoterRector/AuthorizationCheckerToAccessDecisionManagerInVoterRectorTest.php b/rules-tests/Symfony73/Rector/Class_/AuthorizationCheckerToAccessDecisionManagerInVoterRector/AuthorizationCheckerToAccessDecisionManagerInVoterRectorTest.php new file mode 100644 index 000000000..0e2503e46 --- /dev/null +++ b/rules-tests/Symfony73/Rector/Class_/AuthorizationCheckerToAccessDecisionManagerInVoterRector/AuthorizationCheckerToAccessDecisionManagerInVoterRectorTest.php @@ -0,0 +1,28 @@ +doTestFile($filePath); + } + + public static function provideData(): Iterator + { + return self::yieldFilesFromDirectory(__DIR__ . '/Fixture'); + } + + public function provideConfigFilePath(): string + { + return __DIR__ . '/config/configured_rule.php'; + } +} diff --git a/rules-tests/Symfony73/Rector/Class_/AuthorizationCheckerToAccessDecisionManagerInVoterRector/Fixture/authorization_checker_voter.php.inc b/rules-tests/Symfony73/Rector/Class_/AuthorizationCheckerToAccessDecisionManagerInVoterRector/Fixture/authorization_checker_voter.php.inc new file mode 100644 index 000000000..24531ee93 --- /dev/null +++ b/rules-tests/Symfony73/Rector/Class_/AuthorizationCheckerToAccessDecisionManagerInVoterRector/Fixture/authorization_checker_voter.php.inc @@ -0,0 +1,49 @@ +authorizationChecker->isGranted('ROLE_ADMIN'); + } +} +----- +accessDecisionManager->decide($token, ['ROLE_ADMIN']); + } +} diff --git a/rules-tests/Symfony73/Rector/Class_/AuthorizationCheckerToAccessDecisionManagerInVoterRector/config/configured_rule.php b/rules-tests/Symfony73/Rector/Class_/AuthorizationCheckerToAccessDecisionManagerInVoterRector/config/configured_rule.php new file mode 100644 index 000000000..95257aed6 --- /dev/null +++ b/rules-tests/Symfony73/Rector/Class_/AuthorizationCheckerToAccessDecisionManagerInVoterRector/config/configured_rule.php @@ -0,0 +1,11 @@ +withRules([ + AuthorizationCheckerToAccessDecisionManagerInVoterRector::class, + ]); diff --git a/rules/Symfony73/Rector/Class_/AuthorizationCheckerToAccessDecisionManagerInVoterRector.php b/rules/Symfony73/Rector/Class_/AuthorizationCheckerToAccessDecisionManagerInVoterRector.php new file mode 100644 index 000000000..393c73ee8 --- /dev/null +++ b/rules/Symfony73/Rector/Class_/AuthorizationCheckerToAccessDecisionManagerInVoterRector.php @@ -0,0 +1,217 @@ +authorizationChecker->isGranted('ROLE_ADMIN'); + } +} +CODE_SAMPLE + , + <<<'CODE_SAMPLE' +use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface; +use Symfony\Component\Security\Core\Authorization\Voter\Voter; +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; + +final class AuthorizationCheckerVoter extends Voter +{ + public function __construct( + private AccessDecisionManagerInterface $accessDecisionManager + ) {} + + protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token): bool + { + return $this->accessDecisionManager->decide($token, ['ROLE_ADMIN']); + } +} +CODE_SAMPLE + ), + ] + ); + } + + /** + * @return array> + */ + public function getNodeTypes(): array + { + return [Class_::class]; + } + + /** + * @param Class_ $node + */ + public function refactor(Node $node): ?Node + { + if ($node->extends === null || ! $this->isName($node->extends, SymfonyClass::VOTER_CLASS)) { + return null; + } + + $hasChanged = false; + $renamedProperties = []; + + $authorizationCheckerType = new ObjectType( + SymfonyClass::AUTHORIZATION_CHECKER + ); + + // 1) Regular properties + foreach ($node->getProperties() as $property) { + if (! $this->isObjectType($property, $authorizationCheckerType)) { + continue; + } + + $property->type = new FullyQualified( + SymfonyClass::ACCESS_DECISION_MANAGER_INTERFACE + ); + + foreach ($property->props as $prop) { + if ($this->getName($prop) === self::AUTHORIZATION_CHECKER_PROPERTY) { + $prop->name = new Identifier(self::ACCESS_DECISION_MANAGER_PROPERTY); + $renamedProperties[self::AUTHORIZATION_CHECKER_PROPERTY] + = self::ACCESS_DECISION_MANAGER_PROPERTY; + } + } + + $hasChanged = true; + } + + // 2) Promoted properties (constructor) + $constructor = $node->getMethod('__construct'); + if ($constructor instanceof ClassMethod) { + foreach ($constructor->params as $param) { + if ( + $param->type === null + || ! $this->isName($param->type, SymfonyClass::AUTHORIZATION_CHECKER) + ) { + continue; + } + + $param->type = new FullyQualified( + SymfonyClass::ACCESS_DECISION_MANAGER_INTERFACE + ); + + if ( + $param->var instanceof Variable + && $this->getName($param->var) === self::AUTHORIZATION_CHECKER_PROPERTY + ) { + $param->var->name = self::ACCESS_DECISION_MANAGER_PROPERTY; + $renamedProperties[self::AUTHORIZATION_CHECKER_PROPERTY] + = self::ACCESS_DECISION_MANAGER_PROPERTY; + } + + $hasChanged = true; + } + } + + // 3) Replace isGranted() with decide() + $voteMethod = $node->getMethod('voteOnAttribute'); + if ($voteMethod instanceof ClassMethod) { + $this->traverseNodesWithCallable( + $voteMethod, + function (Node $node) use (&$hasChanged, $voteMethod, $renamedProperties) { + if ($node instanceof Class_ || $node instanceof Function_) { + return NodeVisitor::DONT_TRAVERSE_CURRENT_AND_CHILDREN; + } + + if (! $node instanceof MethodCall) { + return null; + } + + if (! $this->isObjectType( + $node->var, + new ObjectType(SymfonyClass::AUTHORIZATION_CHECKER) + )) { + return null; + } + + if (! $node->var instanceof PropertyFetch) { + return null; + } + + if (! $this->isName($node->name, 'isGranted')) { + return null; + } + + $propertyName = $this->getName($node->var->name); + if ($propertyName === null || ! isset($renamedProperties[$propertyName])) { + return null; + } + + $node->var->name = new Identifier($renamedProperties[$propertyName]); + $node->name = new Identifier('decide'); + + $tokenVariable = $voteMethod->params[2]->var ?? null; + if (! $tokenVariable instanceof Variable) { + return null; + } + + $attributeArg = $node->args[0] ?? null; + if (! $attributeArg instanceof Arg) { + return null; + } + + $attributeExpr = $attributeArg->value; + + $node->args = [ + new Arg($tokenVariable), + new Arg(new Array_([ + new ArrayItem($attributeExpr), + ])), + ]; + + $hasChanged = true; + return $node; + } + ); + } + + return $hasChanged ? $node : null; + } +} diff --git a/src/Enum/SymfonyClass.php b/src/Enum/SymfonyClass.php index 4c02cac1b..6c521ff24 100644 --- a/src/Enum/SymfonyClass.php +++ b/src/Enum/SymfonyClass.php @@ -90,6 +90,8 @@ final class SymfonyClass public const string USER_INTERFACE = 'Symfony\Component\Security\Core\User\UserInterface'; + public const string ACCESS_DECISION_MANAGER_INTERFACE = 'Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface'; + public const string UUID = 'Symfony\Component\Uid\Uuid'; public const string ROUTE_COLLECTION_BUILDER = 'Symfony\Component\Routing\RouteCollectionBuilder';