From 00a59a38248776ce746d63b2dbf9288ad8382eca Mon Sep 17 00:00:00 2001 From: Raziel Rodrigues Date: Wed, 5 Mar 2025 21:02:05 +0000 Subject: [PATCH 1/4] feat: creating rate limiter attribute --- .../Bundle/FrameworkBundle/CHANGELOG.md | 1 + .../Attribute/RateLimitAttribute.php | 32 +++++++++++++++++++ 2 files changed, 33 insertions(+) create mode 100644 src/Symfony/Component/RateLimiter/Attribute/RateLimitAttribute.php diff --git a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md index 17201aeaec284..3c7f9b4ab337b 100644 --- a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md +++ b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md @@ -4,6 +4,7 @@ CHANGELOG 7.3 --- + * Add `rate_limiter` controller attribute * Add support for assets pre-compression * Rename `TranslationUpdateCommand` to `TranslationExtractCommand` * Add JsonStreamer services and configuration diff --git a/src/Symfony/Component/RateLimiter/Attribute/RateLimitAttribute.php b/src/Symfony/Component/RateLimiter/Attribute/RateLimitAttribute.php new file mode 100644 index 0000000000000..2a446b3b44b5c --- /dev/null +++ b/src/Symfony/Component/RateLimiter/Attribute/RateLimitAttribute.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\RateLimiter\Attribute; + +/** + * Add the hability to rate limit a method from a controller + * + * @see https://symfony.com/doc/current/rate_limiter.html + * + * @author Raziel Rodrigues + */ +#[\Attribute(\Attribute::IS_REPEATABLE | \Attribute::TARGET_METHOD)] +final class RateLimit +{ + /** + * @param string $limiter The name of the limiter to use + * @param array $methods Methods to apply the rate limit + */ + public function __construct( + public string $limiter, + public array $methods + ) {} +} From 67316e3a675ca0991a5a85cec3fd61f07f06ce6d Mon Sep 17 00:00:00 2001 From: Raziel Rodrigues Date: Mon, 12 May 2025 23:37:00 +0100 Subject: [PATCH 2/4] refactor: changing the name of file --- .../Attribute/{RateLimitAttribute.php => RateLimit.php} | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) rename src/Symfony/Component/RateLimiter/Attribute/{RateLimitAttribute.php => RateLimit.php} (73%) diff --git a/src/Symfony/Component/RateLimiter/Attribute/RateLimitAttribute.php b/src/Symfony/Component/RateLimiter/Attribute/RateLimit.php similarity index 73% rename from src/Symfony/Component/RateLimiter/Attribute/RateLimitAttribute.php rename to src/Symfony/Component/RateLimiter/Attribute/RateLimit.php index 2a446b3b44b5c..0e656781d1dd1 100644 --- a/src/Symfony/Component/RateLimiter/Attribute/RateLimitAttribute.php +++ b/src/Symfony/Component/RateLimiter/Attribute/RateLimit.php @@ -12,7 +12,7 @@ namespace Symfony\Component\RateLimiter\Attribute; /** - * Add the hability to rate limit a method from a controller + * Rate limit the controller. * * @see https://symfony.com/doc/current/rate_limiter.html * @@ -22,11 +22,11 @@ final class RateLimit { /** - * @param string $limiter The name of the limiter to use - * @param array $methods Methods to apply the rate limit + * @param string $limiter The configured limiter name + * @param string[] $methods Request methods to apply the rate limit (`[]` for all) */ public function __construct( public string $limiter, - public array $methods + public array $methods = [] ) {} } From 279217d8005b50ef88f31d20593f15993d9647b3 Mon Sep 17 00:00:00 2001 From: Raziel Rodrigues Date: Mon, 12 May 2025 23:37:10 +0100 Subject: [PATCH 3/4] feat: adding the rate limit attribute listener --- .../RateLimitAttributeListener.php | 109 ++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 src/Symfony/Component/RateLimiter/EventListener/RateLimitAttributeListener.php diff --git a/src/Symfony/Component/RateLimiter/EventListener/RateLimitAttributeListener.php b/src/Symfony/Component/RateLimiter/EventListener/RateLimitAttributeListener.php new file mode 100644 index 0000000000000..93aba2059a441 --- /dev/null +++ b/src/Symfony/Component/RateLimiter/EventListener/RateLimitAttributeListener.php @@ -0,0 +1,109 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\RateLimiter\EventListener; + +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\ExpressionLanguage\Expression; +use Symfony\Component\ExpressionLanguage\ExpressionLanguage; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\Event\ControllerArgumentsEvent; +use Symfony\Component\HttpKernel\Exception\HttpException; +use Symfony\Component\HttpKernel\KernelEvents; +use Symfony\Component\RateLimiter\Attribute\RateLimit; +use Symfony\Component\Security\Core\Authorization\AccessDecision; +use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface; +use Symfony\Component\Security\Core\Exception\AccessDeniedException; +use Symfony\Component\Security\Core\Exception\RuntimeException; + +/** + * Rate limit attribute listener for controller + * + * @see https://symfony.com/doc/current/rate_limiter.html + * + * @author Raziel Rodrigues + */ +class RateLimitAttributeListener implements EventSubscriberInterface +{ + public function __construct( + # private readonly AuthorizationCheckerInterface $authChecker, + private ?ExpressionLanguage $expressionLanguage = null, + ) { + } + + public function onKernelControllerArguments(ControllerArgumentsEvent $event): void + { + /** @var RateLimit[] $attributes */ + if (!\is_array($attributes = $event->getAttributes()[RateLimit::class] ?? null)) { + return; + } + + $request = $event->getRequest(); + $arguments = $event->getNamedArguments(); + + foreach ($attributes as $attribute) { + $subject = null; + + if ($subjectRef = $attribute->subject) { + if (\is_array($subjectRef)) { + foreach ($subjectRef as $refKey => $ref) { + $subject[\is_string($refKey) ? $refKey : (string) $ref] = $this->getRateLimitSubject($ref, $request, $arguments); + } + } else { + $subject = $this->getRateLimitSubject($subjectRef, $request, $arguments); + } + } +/* $accessDecision = new AccessDecision(); + + if (!$accessDecision->isGranted = $this->authChecker->isGranted($attribute->attribute, $subject, $accessDecision)) { + $message = $attribute->message ?: $accessDecision->getMessage(); + + if ($statusCode = $attribute->statusCode) { + throw new HttpException($statusCode, $message, code: $attribute->exceptionCode ?? 0); + } + + $e = new AccessDeniedException($message, code: $attribute->exceptionCode ?? 403); + $e->setAttributes([$attribute->attribute]); + $e->setSubject($subject); + $e->setAccessDecision($accessDecision); + + throw $e; + } */ + } + } + + public static function getSubscribedEvents(): array + { + return [KernelEvents::CONTROLLER_ARGUMENTS => ['onKernelControllerArguments', 20]]; + } + + private function getRateLimitSubject(string|Expression|\Closure $subjectRef, Request $request, array $arguments): mixed + { + if ($subjectRef instanceof \Closure) { + return $subjectRef($arguments, $request); + } + + if ($subjectRef instanceof Expression) { + $this->expressionLanguage ??= new ExpressionLanguage(); + + return $this->expressionLanguage->evaluate($subjectRef, [ + 'request' => $request, + 'args' => $arguments, + ]); + } + + if (!\array_key_exists($subjectRef, $arguments)) { + throw new RuntimeException(\sprintf('Could not find the subject "%s" for the #[RateLimit] attribute. Try adding a "$%s" argument to your controller method.', $subjectRef, $subjectRef)); + } + + return $arguments[$subjectRef]; + } +} From dd2dc8e134517cbaa9d859af238bb3aaf5a57766 Mon Sep 17 00:00:00 2001 From: Raziel Rodrigues Date: Tue, 13 May 2025 11:29:44 +0100 Subject: [PATCH 4/4] refactor: removing usused code --- .../RateLimitAttributeListener.php | 66 +------------------ 1 file changed, 1 insertion(+), 65 deletions(-) diff --git a/src/Symfony/Component/RateLimiter/EventListener/RateLimitAttributeListener.php b/src/Symfony/Component/RateLimiter/EventListener/RateLimitAttributeListener.php index 93aba2059a441..853f83b20ef87 100644 --- a/src/Symfony/Component/RateLimiter/EventListener/RateLimitAttributeListener.php +++ b/src/Symfony/Component/RateLimiter/EventListener/RateLimitAttributeListener.php @@ -12,17 +12,10 @@ namespace Symfony\Component\RateLimiter\EventListener; use Symfony\Component\EventDispatcher\EventSubscriberInterface; -use Symfony\Component\ExpressionLanguage\Expression; use Symfony\Component\ExpressionLanguage\ExpressionLanguage; -use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Event\ControllerArgumentsEvent; -use Symfony\Component\HttpKernel\Exception\HttpException; use Symfony\Component\HttpKernel\KernelEvents; use Symfony\Component\RateLimiter\Attribute\RateLimit; -use Symfony\Component\Security\Core\Authorization\AccessDecision; -use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface; -use Symfony\Component\Security\Core\Exception\AccessDeniedException; -use Symfony\Component\Security\Core\Exception\RuntimeException; /** * Rate limit attribute listener for controller @@ -34,10 +27,8 @@ class RateLimitAttributeListener implements EventSubscriberInterface { public function __construct( - # private readonly AuthorizationCheckerInterface $authChecker, private ?ExpressionLanguage $expressionLanguage = null, - ) { - } + ) {} public function onKernelControllerArguments(ControllerArgumentsEvent $event): void { @@ -45,65 +36,10 @@ public function onKernelControllerArguments(ControllerArgumentsEvent $event): vo if (!\is_array($attributes = $event->getAttributes()[RateLimit::class] ?? null)) { return; } - - $request = $event->getRequest(); - $arguments = $event->getNamedArguments(); - - foreach ($attributes as $attribute) { - $subject = null; - - if ($subjectRef = $attribute->subject) { - if (\is_array($subjectRef)) { - foreach ($subjectRef as $refKey => $ref) { - $subject[\is_string($refKey) ? $refKey : (string) $ref] = $this->getRateLimitSubject($ref, $request, $arguments); - } - } else { - $subject = $this->getRateLimitSubject($subjectRef, $request, $arguments); - } - } -/* $accessDecision = new AccessDecision(); - - if (!$accessDecision->isGranted = $this->authChecker->isGranted($attribute->attribute, $subject, $accessDecision)) { - $message = $attribute->message ?: $accessDecision->getMessage(); - - if ($statusCode = $attribute->statusCode) { - throw new HttpException($statusCode, $message, code: $attribute->exceptionCode ?? 0); - } - - $e = new AccessDeniedException($message, code: $attribute->exceptionCode ?? 403); - $e->setAttributes([$attribute->attribute]); - $e->setSubject($subject); - $e->setAccessDecision($accessDecision); - - throw $e; - } */ - } } public static function getSubscribedEvents(): array { return [KernelEvents::CONTROLLER_ARGUMENTS => ['onKernelControllerArguments', 20]]; } - - private function getRateLimitSubject(string|Expression|\Closure $subjectRef, Request $request, array $arguments): mixed - { - if ($subjectRef instanceof \Closure) { - return $subjectRef($arguments, $request); - } - - if ($subjectRef instanceof Expression) { - $this->expressionLanguage ??= new ExpressionLanguage(); - - return $this->expressionLanguage->evaluate($subjectRef, [ - 'request' => $request, - 'args' => $arguments, - ]); - } - - if (!\array_key_exists($subjectRef, $arguments)) { - throw new RuntimeException(\sprintf('Could not find the subject "%s" for the #[RateLimit] attribute. Try adding a "$%s" argument to your controller method.', $subjectRef, $subjectRef)); - } - - return $arguments[$subjectRef]; - } }