feat(rate-limit): Allow overwriting the rate limit

Signed-off-by: Joas Schilling <coding@schilljs.com>
pull/56382/head
Joas Schilling 2025-11-07 16:56:09 +07:00
parent 72dd55e53e
commit 826fe1a918
No known key found for this signature in database
GPG Key ID: F72FA5B49FFA96B0
3 changed files with 79 additions and 3 deletions

@ -463,6 +463,30 @@ $CONFIG = [
*/
'ratelimit.protection.enabled' => true,
/**
* Overwrite the individual rate limit for a specific route
*
* From time to time it can be necessary to extend the rate limit of a specific route,
* depending on your usage pattern or when you script some actions.
* Instead of completely disabling the rate limit or excluding an IP address from the
* rate limit, the following config allows to overwrite the rate limit duration and period.
*
* The first level key is the name of the route. You can find the route name from a URL
* using the ``occ router:list`` command of your server.
*
* You can also specify different limits for logged-in users with the ``user`` key
* and not-logged-in users with the ``anon`` key. However, if there is no specific ``user`` limit,
* the ``anon`` limit is also applied for logged-in users.
*
* Defaults to empty array ``[]``
*/
'ratelimit_overwrite' => [
'profile.profilepage.index' => [
'user' => ['limit' => 300, 'period' => 3600],
'anon' => ['limit' => 1, 'period' => 300],
]
],
/**
* Size of subnet used to normalize IPv6
*

@ -22,9 +22,11 @@ use OCP\AppFramework\Http\Response;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\AppFramework\Middleware;
use OCP\IAppConfig;
use OCP\IConfig;
use OCP\IRequest;
use OCP\ISession;
use OCP\IUserSession;
use Psr\Log\LoggerInterface;
use ReflectionMethod;
/**
@ -56,7 +58,9 @@ class RateLimitingMiddleware extends Middleware {
protected Limiter $limiter,
protected ISession $session,
protected IAppConfig $appConfig,
protected IConfig $serverConfig,
protected BruteforceAllowList $bruteForceAllowList,
protected LoggerInterface $logger,
) {
}
@ -74,7 +78,13 @@ class RateLimitingMiddleware extends Middleware {
}
if ($this->userSession->isLoggedIn()) {
$rateLimit = $this->readLimitFromAnnotationOrAttribute($controller, $methodName, 'UserRateThrottle', UserRateLimit::class);
$rateLimit = $this->readLimitFromAnnotationOrAttribute(
$controller,
$methodName,
'UserRateThrottle',
UserRateLimit::class,
'user',
);
if ($rateLimit !== null) {
if ($this->appConfig->getValueBool('bruteforcesettings', 'apply_allowlist_to_ratelimit')
@ -94,7 +104,13 @@ class RateLimitingMiddleware extends Middleware {
// If not user specific rate limit is found the Anon rate limit applies!
}
$rateLimit = $this->readLimitFromAnnotationOrAttribute($controller, $methodName, 'AnonRateThrottle', AnonRateLimit::class);
$rateLimit = $this->readLimitFromAnnotationOrAttribute(
$controller,
$methodName,
'AnonRateThrottle',
AnonRateLimit::class,
'anon',
);
if ($rateLimit !== null) {
$this->limiter->registerAnonRequest(
@ -115,7 +131,35 @@ class RateLimitingMiddleware extends Middleware {
* @param class-string<T> $attributeClass
* @return ?ARateLimit
*/
protected function readLimitFromAnnotationOrAttribute(Controller $controller, string $methodName, string $annotationName, string $attributeClass): ?ARateLimit {
protected function readLimitFromAnnotationOrAttribute(Controller $controller, string $methodName, string $annotationName, string $attributeClass, string $overwriteKey): ?ARateLimit {
$rateLimitOverwrite = $this->serverConfig->getSystemValue('ratelimit_overwrite', []);
if (!empty($rateLimitOverwrite)) {
$controllerRef = new \ReflectionClass($controller);
$appName = $controllerRef->getProperty('appName')->getValue($controller);
$controllerName = substr($controller::class, strrpos($controller::class, '\\') + 1);
$controllerName = substr($controllerName, 0, 0 - strlen('Controller'));
$overwriteConfig = strtolower($appName . '.' . $controllerName . '.' . $methodName);
$rateLimitOverwriteForActionAndType = $rateLimitOverwrite[$overwriteConfig][$overwriteKey] ?? null;
if ($rateLimitOverwriteForActionAndType !== null) {
$isValid = isset($rateLimitOverwriteForActionAndType['limit'], $rateLimitOverwriteForActionAndType['period'])
&& $rateLimitOverwriteForActionAndType['limit'] > 0
&& $rateLimitOverwriteForActionAndType['period'] > 0;
if ($isValid) {
return new $attributeClass(
(int)$rateLimitOverwriteForActionAndType['limit'],
(int)$rateLimitOverwriteForActionAndType['period'],
);
}
$this->logger->warning('Rate limit overwrite on controller "{overwriteConfig}" for "{overwriteKey}" is invalid', [
'overwriteConfig' => $overwriteConfig,
'overwriteKey' => $overwriteKey,
]);
}
}
$annotationLimit = $this->reflector->getAnnotationParameter($annotationName, 'limit');
$annotationPeriod = $this->reflector->getAnnotationParameter($annotationName, 'period');

@ -20,11 +20,13 @@ use OCP\AppFramework\Http\Attribute\UserRateLimit;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\IAppConfig;
use OCP\IConfig;
use OCP\IRequest;
use OCP\ISession;
use OCP\IUser;
use OCP\IUserSession;
use PHPUnit\Framework\MockObject\MockObject;
use Psr\Log\LoggerInterface;
use Test\TestCase;
class TestRateLimitController extends Controller {
@ -64,7 +66,9 @@ class RateLimitingMiddlewareTest extends TestCase {
private Limiter|MockObject $limiter;
private ISession|MockObject $session;
private IAppConfig|MockObject $appConfig;
private IConfig|MockObject $serverConfig;
private BruteforceAllowList|MockObject $bruteForceAllowList;
private LoggerInterface|MockObject $logger;
private RateLimitingMiddleware $rateLimitingMiddleware;
protected function setUp(): void {
@ -76,7 +80,9 @@ class RateLimitingMiddlewareTest extends TestCase {
$this->limiter = $this->createMock(Limiter::class);
$this->session = $this->createMock(ISession::class);
$this->appConfig = $this->createMock(IAppConfig::class);
$this->serverConfig = $this->createMock(IConfig::class);
$this->bruteForceAllowList = $this->createMock(BruteforceAllowList::class);
$this->logger = $this->createMock(LoggerInterface::class);
$this->rateLimitingMiddleware = new RateLimitingMiddleware(
$this->request,
@ -85,7 +91,9 @@ class RateLimitingMiddlewareTest extends TestCase {
$this->limiter,
$this->session,
$this->appConfig,
$this->serverConfig,
$this->bruteForceAllowList,
$this->logger
);
}