Skip to content

Commit 2224d78

Browse files
committed
[Security][RateLimiter] Allow to use no lock in the rate limiter/login throttling
1 parent d54a122 commit 2224d78

File tree

8 files changed

+65
-8
lines changed

8 files changed

+65
-8
lines changed

UPGRADE-5.3.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,8 @@ Security
8383
SecurityBundle
8484
--------------
8585

86+
* [BC break] Add `login_throttling.lock_factory` setting defaulting to `null`. Set this option
87+
to `lock.factory` if you need precise login rate limiting with synchronous requests.
8688
* Deprecate `UserPasswordEncoderCommand` class and the corresponding `user:encode-password` command,
8789
use `UserPasswordHashCommand` and `user:hash-password` instead
8890
* Deprecate the `security.encoder_factory.generic` service, the `security.encoder_factory` and `Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface` aliases,

src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1860,7 +1860,7 @@ private function addRateLimiterSection(ArrayNodeDefinition $rootNode)
18601860
->arrayPrototype()
18611861
->children()
18621862
->scalarNode('lock_factory')
1863-
->info('The service ID of the lock factory used by this limiter')
1863+
->info('The service ID of the lock factory used by this limiter (or null to disable locking)')
18641864
->defaultValue('lock.factory')
18651865
->end()
18661866
->scalarNode('cache_pool')

src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ class FrameworkExtension extends Extension
200200
private $mailerConfigEnabled = false;
201201
private $httpClientConfigEnabled = false;
202202
private $notifierConfigEnabled = false;
203-
private $lockConfigEnabled = false;
203+
private static $lockConfigEnabled = false;
204204

205205
/**
206206
* Responds to the app.config configuration parameter.
@@ -433,7 +433,7 @@ public function load(array $configs, ContainerBuilder $container)
433433
$this->registerPropertyInfoConfiguration($container, $loader);
434434
}
435435

436-
if ($this->lockConfigEnabled = $this->isConfigEnabled($container, $config['lock'])) {
436+
if (self::$lockConfigEnabled = $this->isConfigEnabled($container, $config['lock'])) {
437437
$this->registerLockConfiguration($config['lock'], $container, $loader);
438438
}
439439

@@ -2327,10 +2327,6 @@ private function registerNotifierConfiguration(array $config, ContainerBuilder $
23272327

23282328
private function registerRateLimiterConfiguration(array $config, ContainerBuilder $container, PhpFileLoader $loader)
23292329
{
2330-
if (!$this->lockConfigEnabled) {
2331-
throw new LogicException('Rate limiter support cannot be enabled without enabling the Lock component.');
2332-
}
2333-
23342330
$loader->load('rate_limiter.php');
23352331

23362332
foreach ($config['limiters'] as $name => $limiterConfig) {
@@ -2345,7 +2341,13 @@ public static function registerRateLimiter(ContainerBuilder $container, string $
23452341

23462342
$limiter = $container->setDefinition($limiterId = 'limiter.'.$name, new ChildDefinition('limiter'));
23472343

2348-
$limiter->addArgument(new Reference($limiterConfig['lock_factory']));
2344+
if (null !== $limiterConfig['lock_factory']) {
2345+
if (!self::$lockConfigEnabled) {
2346+
throw new LogicException(sprintf('Rate limiter "%s" requires the Lock component to be installed and configured.', $name));
2347+
}
2348+
2349+
$limiter->replaceArgument(2, new Reference($limiterConfig['lock_factory']));
2350+
}
23492351
unset($limiterConfig['lock_factory']);
23502352

23512353
$storageId = $limiterConfig['storage_service'] ?? null;

src/Symfony/Bundle/FrameworkBundle/Resources/config/rate_limiter.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
->args([
2525
abstract_arg('config'),
2626
abstract_arg('storage'),
27+
null,
2728
])
2829
;
2930
};

src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/PhpFrameworkExtensionTest.php

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313

1414
use Symfony\Component\Config\FileLocator;
1515
use Symfony\Component\DependencyInjection\ContainerBuilder;
16+
use Symfony\Component\DependencyInjection\Exception\LogicException;
17+
use Symfony\Component\DependencyInjection\Exception\OutOfBoundsException;
1618
use Symfony\Component\DependencyInjection\Loader\PhpFileLoader;
1719
use Symfony\Component\Workflow\Exception\InvalidDefinitionException;
1820

@@ -82,4 +84,50 @@ public function testWorkflowValidationStateMachine()
8284
]);
8385
});
8486
}
87+
88+
public function testRateLimiterWithLockFactory()
89+
{
90+
try {
91+
$this->createContainerFromClosure(function (ContainerBuilder $container) {
92+
$container->loadFromExtension('framework', [
93+
'rate_limiter' => [
94+
'with_lock' => ['policy' => 'fixed_window', 'limit' => 10, 'interval' => '1 hour'],
95+
],
96+
]);
97+
});
98+
99+
$this->fail('No LogicException thrown');
100+
} catch (LogicException $e) {
101+
$this->assertEquals('Rate limiter "with_lock" requires the Lock component to be installed and configured.', $e->getMessage());
102+
}
103+
104+
$container = $this->createContainerFromClosure(function (ContainerBuilder $container) {
105+
$container->loadFromExtension('framework', [
106+
'lock' => true,
107+
'rate_limiter' => [
108+
'with_lock' => ['policy' => 'fixed_window', 'limit' => 10, 'interval' => '1 hour'],
109+
],
110+
]);
111+
});
112+
113+
$withLock = $container->getDefinition('limiter.with_lock');
114+
$this->assertEquals('lock.factory', (string) $withLock->getArgument(2));
115+
}
116+
117+
public function testRateLimiterLockFactory()
118+
{
119+
$this->markTestSkipped(
120+
$this->expectException(OutOfBoundsException::class);
121+
$this->expectExceptionMessage('The argument "2" doesn\'t exist.');
122+
123+
$container = $this->createContainerFromClosure(function (ContainerBuilder $container) {
124+
$container->loadFromExtension('framework', [
125+
'rate_limiter' => [
126+
'without_lock' => ['policy' => 'fixed_window', 'limit' => 10, 'interval' => '1 hour', 'lock_factory' => null],
127+
],
128+
]);
129+
});
130+
131+
$container->getDefinition('limiter.without_lock')->getArgument(2);
132+
}
85133
}

src/Symfony/Bundle/FrameworkBundle/composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
"symfony/messenger": "^5.2",
5252
"symfony/mime": "^4.4|^5.0",
5353
"symfony/process": "^4.4|^5.0",
54+
"symfony/rate-limiter": "^5.2",
5455
"symfony/security-bundle": "^5.3",
5556
"symfony/serializer": "^5.2",
5657
"symfony/stopwatch": "^4.4|^5.0",

src/Symfony/Bundle/SecurityBundle/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ CHANGELOG
44
5.3
55
---
66

7+
* [BC break] Add `login_throttling.lock_factory` setting defaulting to `null` (instead of `lock.factory`)
78
* Add the `debug:firewall` command.
89
* Deprecate `UserPasswordEncoderCommand` class and the corresponding `user:encode-password` command,
910
use `UserPasswordHashCommand` and `user:hash-password` instead

src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/LoginThrottlingFactory.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ public function addConfiguration(NodeDefinition $builder)
5454
->children()
5555
->scalarNode('limiter')->info(sprintf('A service id implementing "%s".', RequestRateLimiterInterface::class))->end()
5656
->integerNode('max_attempts')->defaultValue(5)->end()
57+
->scalarNode('lock_factory')->info('The service ID of the lock factory used by the login rate limiter (or null to disable locking)')->defaultNull()->end()
5758
->end();
5859
}
5960

@@ -76,6 +77,7 @@ public function createAuthenticator(ContainerBuilder $container, string $firewal
7677
'policy' => 'fixed_window',
7778
'limit' => $config['max_attempts'],
7879
'interval' => '1 minute',
80+
'lock_factory' => $config['lock_factory'],
7981
];
8082
FrameworkExtension::registerRateLimiter($container, $localId = '_login_local_'.$firewallName, $limiterOptions);
8183

0 commit comments

Comments
 (0)