From f416cacc64f26b88181ae6542e92ed1e5621ccd7 Mon Sep 17 00:00:00 2001 From: Lukas Reschke Date: Mon, 6 Sep 2021 16:31:01 +0200 Subject: Add database ratelimiting backend In case no distributed memory cache is specified this adds a database backend for ratelimit purposes. Signed-off-by: Lukas Reschke --- core/Migrations/Version23000Date20210906132259.php | 44 +++++++ lib/composer/composer/autoload_classmap.php | 4 +- lib/composer/composer/autoload_static.php | 4 +- .../RateLimiting/Backend/DatabaseBackend.php | 124 +++++++++++++++++ .../Security/RateLimiting/Backend/IBackend.php | 6 +- .../Security/RateLimiting/Backend/MemoryCache.php | 129 ------------------ .../RateLimiting/Backend/MemoryCacheBackend.php | 127 ++++++++++++++++++ lib/private/Security/RateLimiting/Limiter.php | 12 +- lib/private/Server.php | 18 ++- .../Backend/MemoryCacheBackendTest.php | 146 +++++++++++++++++++++ .../RateLimiting/Backend/MemoryCacheTest.php | 146 --------------------- tests/lib/Security/RateLimiting/LimiterTest.php | 29 +--- version.php | 2 +- 13 files changed, 473 insertions(+), 318 deletions(-) create mode 100644 core/Migrations/Version23000Date20210906132259.php create mode 100644 lib/private/Security/RateLimiting/Backend/DatabaseBackend.php delete mode 100644 lib/private/Security/RateLimiting/Backend/MemoryCache.php create mode 100644 lib/private/Security/RateLimiting/Backend/MemoryCacheBackend.php create mode 100644 tests/lib/Security/RateLimiting/Backend/MemoryCacheBackendTest.php delete mode 100644 tests/lib/Security/RateLimiting/Backend/MemoryCacheTest.php diff --git a/core/Migrations/Version23000Date20210906132259.php b/core/Migrations/Version23000Date20210906132259.php new file mode 100644 index 00000000000..26d18edc8f1 --- /dev/null +++ b/core/Migrations/Version23000Date20210906132259.php @@ -0,0 +1,44 @@ +hasTable(self::TABLE_NAME); + + if (!$hasTable) { + $table = $schema->createTable(self::TABLE_NAME); + $table->addColumn('hash', Types::STRING, [ + 'notnull' => true, + 'length' => 128, + ]); + $table->addColumn('delete_after', Types::DATETIME, [ + 'notnull' => true, + ]); + $table->addIndex(['hash'], 'ratelimit_hash'); + $table->addIndex(['delete_after'], 'ratelimit_delete_after'); + return $schema; + } + + return null; + } +} diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index b70bdb4c55d..cea7158b1b6 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -959,6 +959,7 @@ return array( 'OC\\Core\\Migrations\\Version21000Date20210119195004' => $baseDir . '/core/Migrations/Version21000Date20210119195004.php', 'OC\\Core\\Migrations\\Version21000Date20210309185126' => $baseDir . '/core/Migrations/Version21000Date20210309185126.php', 'OC\\Core\\Migrations\\Version21000Date20210309185127' => $baseDir . '/core/Migrations/Version21000Date20210309185127.php', + 'OC\\Core\\Migrations\\Version23000Date20210906132259' => $baseDir . '/core/Migrations/Version23000Date20210906132259.php', 'OC\\Core\\Notification\\CoreNotifier' => $baseDir . '/core/Notification/CoreNotifier.php', 'OC\\Core\\Service\\LoginFlowV2Service' => $baseDir . '/core/Service/LoginFlowV2Service.php', 'OC\\DB\\Adapter' => $baseDir . '/lib/private/DB/Adapter.php', @@ -1356,8 +1357,9 @@ return array( 'OC\\Security\\IdentityProof\\Manager' => $baseDir . '/lib/private/Security/IdentityProof/Manager.php', 'OC\\Security\\IdentityProof\\Signer' => $baseDir . '/lib/private/Security/IdentityProof/Signer.php', 'OC\\Security\\Normalizer\\IpAddress' => $baseDir . '/lib/private/Security/Normalizer/IpAddress.php', + 'OC\\Security\\RateLimiting\\Backend\\DatabaseBackend' => $baseDir . '/lib/private/Security/RateLimiting/Backend/DatabaseBackend.php', 'OC\\Security\\RateLimiting\\Backend\\IBackend' => $baseDir . '/lib/private/Security/RateLimiting/Backend/IBackend.php', - 'OC\\Security\\RateLimiting\\Backend\\MemoryCache' => $baseDir . '/lib/private/Security/RateLimiting/Backend/MemoryCache.php', + 'OC\\Security\\RateLimiting\\Backend\\MemoryCacheBackend' => $baseDir . '/lib/private/Security/RateLimiting/Backend/MemoryCacheBackend.php', 'OC\\Security\\RateLimiting\\Exception\\RateLimitExceededException' => $baseDir . '/lib/private/Security/RateLimiting/Exception/RateLimitExceededException.php', 'OC\\Security\\RateLimiting\\Limiter' => $baseDir . '/lib/private/Security/RateLimiting/Limiter.php', 'OC\\Security\\SecureRandom' => $baseDir . '/lib/private/Security/SecureRandom.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index 5c6dff0353e..1bfaea25c68 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -988,6 +988,7 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c 'OC\\Core\\Migrations\\Version21000Date20210119195004' => __DIR__ . '/../../..' . '/core/Migrations/Version21000Date20210119195004.php', 'OC\\Core\\Migrations\\Version21000Date20210309185126' => __DIR__ . '/../../..' . '/core/Migrations/Version21000Date20210309185126.php', 'OC\\Core\\Migrations\\Version21000Date20210309185127' => __DIR__ . '/../../..' . '/core/Migrations/Version21000Date20210309185127.php', + 'OC\\Core\\Migrations\\Version23000Date20210906132259' => __DIR__ . '/../../..' . '/core/Migrations/Version23000Date20210906132259.php', 'OC\\Core\\Notification\\CoreNotifier' => __DIR__ . '/../../..' . '/core/Notification/CoreNotifier.php', 'OC\\Core\\Service\\LoginFlowV2Service' => __DIR__ . '/../../..' . '/core/Service/LoginFlowV2Service.php', 'OC\\DB\\Adapter' => __DIR__ . '/../../..' . '/lib/private/DB/Adapter.php', @@ -1385,8 +1386,9 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c 'OC\\Security\\IdentityProof\\Manager' => __DIR__ . '/../../..' . '/lib/private/Security/IdentityProof/Manager.php', 'OC\\Security\\IdentityProof\\Signer' => __DIR__ . '/../../..' . '/lib/private/Security/IdentityProof/Signer.php', 'OC\\Security\\Normalizer\\IpAddress' => __DIR__ . '/../../..' . '/lib/private/Security/Normalizer/IpAddress.php', + 'OC\\Security\\RateLimiting\\Backend\\DatabaseBackend' => __DIR__ . '/../../..' . '/lib/private/Security/RateLimiting/Backend/DatabaseBackend.php', 'OC\\Security\\RateLimiting\\Backend\\IBackend' => __DIR__ . '/../../..' . '/lib/private/Security/RateLimiting/Backend/IBackend.php', - 'OC\\Security\\RateLimiting\\Backend\\MemoryCache' => __DIR__ . '/../../..' . '/lib/private/Security/RateLimiting/Backend/MemoryCache.php', + 'OC\\Security\\RateLimiting\\Backend\\MemoryCacheBackend' => __DIR__ . '/../../..' . '/lib/private/Security/RateLimiting/Backend/MemoryCacheBackend.php', 'OC\\Security\\RateLimiting\\Exception\\RateLimitExceededException' => __DIR__ . '/../../..' . '/lib/private/Security/RateLimiting/Exception/RateLimitExceededException.php', 'OC\\Security\\RateLimiting\\Limiter' => __DIR__ . '/../../..' . '/lib/private/Security/RateLimiting/Limiter.php', 'OC\\Security\\SecureRandom' => __DIR__ . '/../../..' . '/lib/private/Security/SecureRandom.php', diff --git a/lib/private/Security/RateLimiting/Backend/DatabaseBackend.php b/lib/private/Security/RateLimiting/Backend/DatabaseBackend.php new file mode 100644 index 00000000000..53d91238673 --- /dev/null +++ b/lib/private/Security/RateLimiting/Backend/DatabaseBackend.php @@ -0,0 +1,124 @@ + + * + * @author Lukas Reschke + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ +namespace OC\Security\RateLimiting\Backend; + +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\IDBConnection; + +class DatabaseBackend implements IBackend { + private const TABLE_NAME = 'ratelimit_entries'; + + /** @var IDBConnection */ + private $dbConnection; + /** @var ITimeFactory */ + private $timeFactory; + + /** + * @param IDBConnection $dbConnection + * @param ITimeFactory $timeFactory + */ + public function __construct( + IDBConnection $dbConnection, + ITimeFactory $timeFactory + ) { + $this->dbConnection = $dbConnection; + $this->timeFactory = $timeFactory; + } + + /** + * @param string $methodIdentifier + * @param string $userIdentifier + * @return string + */ + private function hash(string $methodIdentifier, + string $userIdentifier): string { + return hash('sha512', $methodIdentifier . $userIdentifier); + } + + /** + * @param string $identifier + * @param int $seconds + * @return int + * @throws \OCP\DB\Exception + */ + private function getExistingAttemptCount( + string $identifier + ): int { + $currentTime = $this->timeFactory->getDateTime(); + + $qb = $this->dbConnection->getQueryBuilder(); + $qb->delete(self::TABLE_NAME) + ->where( + $qb->expr()->lte('delete_after', $qb->createNamedParameter($currentTime, IQueryBuilder::PARAM_DATE)) + ) + ->execute(); + + $qb = $this->dbConnection->getQueryBuilder(); + $qb->select($qb->func()->count()) + ->from(self::TABLE_NAME) + ->where( + $qb->expr()->eq('hash', $qb->createNamedParameter($identifier, IQueryBuilder::PARAM_STR)) + ) + ->andWhere( + $qb->expr()->gte('delete_after', $qb->createNamedParameter($currentTime, IQueryBuilder::PARAM_DATE)) + ); + + $cursor = $qb->execute(); + $row = $cursor->fetchOne(); + $cursor->closeCursor(); + + return (int)$row; + } + + /** + * {@inheritDoc} + */ + public function getAttempts(string $methodIdentifier, + string $userIdentifier): int { + $identifier = $this->hash($methodIdentifier, $userIdentifier); + return $this->getExistingAttemptCount($identifier); + } + + /** + * {@inheritDoc} + */ + public function registerAttempt(string $methodIdentifier, + string $userIdentifier, + int $period) { + $identifier = $this->hash($methodIdentifier, $userIdentifier); + $deleteAfter = $this->timeFactory->getDateTime()->add(new \DateInterval("PT{$period}S")); + + $qb = $this->dbConnection->getQueryBuilder(); + + $qb->insert(self::TABLE_NAME) + ->values([ + 'hash' => $qb->createNamedParameter($identifier, IQueryBuilder::PARAM_STR), + 'delete_after' => $qb->createNamedParameter($deleteAfter, IQueryBuilder::PARAM_DATE), + ]) + ->execute(); + } +} diff --git a/lib/private/Security/RateLimiting/Backend/IBackend.php b/lib/private/Security/RateLimiting/Backend/IBackend.php index 57dd4e3cc6d..09ab8e46956 100644 --- a/lib/private/Security/RateLimiting/Backend/IBackend.php +++ b/lib/private/Security/RateLimiting/Backend/IBackend.php @@ -36,16 +36,14 @@ namespace OC\Security\RateLimiting\Backend; */ interface IBackend { /** - * Gets the amount of attempts within the last specified seconds + * Gets the number of attempts for the specified method * * @param string $methodIdentifier Identifier for the method * @param string $userIdentifier Identifier for the user - * @param int $seconds Seconds to look back at * @return int */ public function getAttempts(string $methodIdentifier, - string $userIdentifier, - int $seconds): int; + string $userIdentifier): int; /** * Registers an attempt diff --git a/lib/private/Security/RateLimiting/Backend/MemoryCache.php b/lib/private/Security/RateLimiting/Backend/MemoryCache.php deleted file mode 100644 index 2893d31ece2..00000000000 --- a/lib/private/Security/RateLimiting/Backend/MemoryCache.php +++ /dev/null @@ -1,129 +0,0 @@ - - * - * @author Christoph Wurst - * @author Lukas Reschke - * @author Morris Jobke - * @author Roeland Jago Douma - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * - */ - -namespace OC\Security\RateLimiting\Backend; - -use OCP\AppFramework\Utility\ITimeFactory; -use OCP\ICache; -use OCP\ICacheFactory; - -/** - * Class MemoryCache uses the configured distributed memory cache for storing - * rate limiting data. - * - * @package OC\Security\RateLimiting\Backend - */ -class MemoryCache implements IBackend { - /** @var ICache */ - private $cache; - /** @var ITimeFactory */ - private $timeFactory; - - /** - * @param ICacheFactory $cacheFactory - * @param ITimeFactory $timeFactory - */ - public function __construct(ICacheFactory $cacheFactory, - ITimeFactory $timeFactory) { - $this->cache = $cacheFactory->createDistributed(__CLASS__); - $this->timeFactory = $timeFactory; - } - - /** - * @param string $methodIdentifier - * @param string $userIdentifier - * @return string - */ - private function hash(string $methodIdentifier, - string $userIdentifier): string { - return hash('sha512', $methodIdentifier . $userIdentifier); - } - - /** - * @param string $identifier - * @return array - */ - private function getExistingAttempts(string $identifier): array { - $cachedAttempts = $this->cache->get($identifier); - if ($cachedAttempts === null) { - return []; - } - - $cachedAttempts = json_decode($cachedAttempts, true); - if (\is_array($cachedAttempts)) { - return $cachedAttempts; - } - - return []; - } - - /** - * {@inheritDoc} - */ - public function getAttempts(string $methodIdentifier, - string $userIdentifier, - int $seconds): int { - $identifier = $this->hash($methodIdentifier, $userIdentifier); - $existingAttempts = $this->getExistingAttempts($identifier); - - $count = 0; - $currentTime = $this->timeFactory->getTime(); - /** @var array $existingAttempts */ - foreach ($existingAttempts as $attempt) { - if (($attempt + $seconds) > $currentTime) { - $count++; - } - } - - return $count; - } - - /** - * {@inheritDoc} - */ - public function registerAttempt(string $methodIdentifier, - string $userIdentifier, - int $period) { - $identifier = $this->hash($methodIdentifier, $userIdentifier); - $existingAttempts = $this->getExistingAttempts($identifier); - $currentTime = $this->timeFactory->getTime(); - - // Unset all attempts older than $period - foreach ($existingAttempts as $key => $attempt) { - if (($attempt + $period) < $currentTime) { - unset($existingAttempts[$key]); - } - } - $existingAttempts = array_values($existingAttempts); - - // Store the new attempt - $existingAttempts[] = (string)$currentTime; - $this->cache->set($identifier, json_encode($existingAttempts)); - } -} diff --git a/lib/private/Security/RateLimiting/Backend/MemoryCacheBackend.php b/lib/private/Security/RateLimiting/Backend/MemoryCacheBackend.php new file mode 100644 index 00000000000..f56df1c9e58 --- /dev/null +++ b/lib/private/Security/RateLimiting/Backend/MemoryCacheBackend.php @@ -0,0 +1,127 @@ + + * + * @author Christoph Wurst + * @author Lukas Reschke + * @author Morris Jobke + * @author Roeland Jago Douma + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OC\Security\RateLimiting\Backend; + +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\ICache; +use OCP\ICacheFactory; + +/** + * Class MemoryCacheBackend uses the configured distributed memory cache for storing + * rate limiting data. + * + * @package OC\Security\RateLimiting\Backend + */ +class MemoryCacheBackend implements IBackend { + /** @var ICache */ + private $cache; + /** @var ITimeFactory */ + private $timeFactory; + + /** + * @param ICacheFactory $cacheFactory + * @param ITimeFactory $timeFactory + */ + public function __construct(ICacheFactory $cacheFactory, + ITimeFactory $timeFactory) { + $this->cache = $cacheFactory->createDistributed(__CLASS__); + $this->timeFactory = $timeFactory; + } + + /** + * @param string $methodIdentifier + * @param string $userIdentifier + * @return string + */ + private function hash(string $methodIdentifier, + string $userIdentifier): string { + return hash('sha512', $methodIdentifier . $userIdentifier); + } + + /** + * @param string $identifier + * @return array + */ + private function getExistingAttempts(string $identifier): array { + $cachedAttempts = $this->cache->get($identifier); + if ($cachedAttempts === null) { + return []; + } + + $cachedAttempts = json_decode($cachedAttempts, true); + if (\is_array($cachedAttempts)) { + return $cachedAttempts; + } + + return []; + } + + /** + * {@inheritDoc} + */ + public function getAttempts(string $methodIdentifier, + string $userIdentifier): int { + $identifier = $this->hash($methodIdentifier, $userIdentifier); + $existingAttempts = $this->getExistingAttempts($identifier); + + $count = 0; + $currentTime = $this->timeFactory->getTime(); + foreach ($existingAttempts as $expirationTime) { + if ($expirationTime > $currentTime) { + $count++; + } + } + + return $count; + } + + /** + * {@inheritDoc} + */ + public function registerAttempt(string $methodIdentifier, + string $userIdentifier, + int $period) { + $identifier = $this->hash($methodIdentifier, $userIdentifier); + $existingAttempts = $this->getExistingAttempts($identifier); + $currentTime = $this->timeFactory->getTime(); + + // Unset all attempts that are already expired + foreach ($existingAttempts as $key => $expirationTime) { + if ($expirationTime < $currentTime) { + unset($existingAttempts[$key]); + } + } + $existingAttempts = array_values($existingAttempts); + + // Store the new attempt + $existingAttempts[] = (string)($currentTime + $period); + $this->cache->set($identifier, json_encode($existingAttempts)); + } +} diff --git a/lib/private/Security/RateLimiting/Limiter.php b/lib/private/Security/RateLimiting/Limiter.php index 26671f52301..e6b8d95bfa2 100644 --- a/lib/private/Security/RateLimiting/Limiter.php +++ b/lib/private/Security/RateLimiting/Limiter.php @@ -30,23 +30,17 @@ namespace OC\Security\RateLimiting; use OC\Security\Normalizer\IpAddress; use OC\Security\RateLimiting\Backend\IBackend; use OC\Security\RateLimiting\Exception\RateLimitExceededException; -use OCP\AppFramework\Utility\ITimeFactory; use OCP\IUser; class Limiter { /** @var IBackend */ private $backend; - /** @var ITimeFactory */ - private $timeFactory; /** - * @param ITimeFactory $timeFactory * @param IBackend $backend */ - public function __construct(ITimeFactory $timeFactory, - IBackend $backend) { + public function __construct(IBackend $backend) { $this->backend = $backend; - $this->timeFactory = $timeFactory; } /** @@ -60,12 +54,12 @@ class Limiter { string $userIdentifier, int $period, int $limit): void { - $existingAttempts = $this->backend->getAttempts($methodIdentifier, $userIdentifier, $period); + $existingAttempts = $this->backend->getAttempts($methodIdentifier, $userIdentifier); if ($existingAttempts >= $limit) { throw new RateLimitExceededException(); } - $this->backend->registerAttempt($methodIdentifier, $userIdentifier, $this->timeFactory->getTime()); + $this->backend->registerAttempt($methodIdentifier, $userIdentifier, $period); } /** diff --git a/lib/private/Server.php b/lib/private/Server.php index 6a1550f83e0..d947fa6f3e9 100644 --- a/lib/private/Server.php +++ b/lib/private/Server.php @@ -782,10 +782,20 @@ class Server extends ServerContainer implements IServerContainer { $this->registerDeprecatedAlias('Search', ISearch::class); $this->registerService(\OC\Security\RateLimiting\Backend\IBackend::class, function ($c) { - return new \OC\Security\RateLimiting\Backend\MemoryCache( - $this->get(ICacheFactory::class), - new \OC\AppFramework\Utility\TimeFactory() - ); + $cacheFactory = $c->get(ICacheFactory::class); + if ($cacheFactory->isAvailable()) { + $backend = new \OC\Security\RateLimiting\Backend\MemoryCacheBackend( + $this->get(ICacheFactory::class), + new \OC\AppFramework\Utility\TimeFactory() + ); + } else { + $backend = new \OC\Security\RateLimiting\Backend\DatabaseBackend( + $c->get(IDBConnection::class), + new \OC\AppFramework\Utility\TimeFactory() + ); + } + + return $backend; }); $this->registerAlias(\OCP\Security\ISecureRandom::class, SecureRandom::class); diff --git a/tests/lib/Security/RateLimiting/Backend/MemoryCacheBackendTest.php b/tests/lib/Security/RateLimiting/Backend/MemoryCacheBackendTest.php new file mode 100644 index 00000000000..970ea9aa5e5 --- /dev/null +++ b/tests/lib/Security/RateLimiting/Backend/MemoryCacheBackendTest.php @@ -0,0 +1,146 @@ + + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace Test\Security\RateLimiting\Backend; + +use OC\Security\RateLimiting\Backend\MemoryCacheBackend; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\ICache; +use OCP\ICacheFactory; +use Test\TestCase; + +class MemoryCacheBackendTest extends TestCase { + /** @var ICacheFactory|\PHPUnit\Framework\MockObject\MockObject */ + private $cacheFactory; + /** @var ITimeFactory|\PHPUnit\Framework\MockObject\MockObject */ + private $timeFactory; + /** @var ICache|\PHPUnit\Framework\MockObject\MockObject */ + private $cache; + /** @var MemoryCacheBackend */ + private $memoryCache; + + protected function setUp(): void { + parent::setUp(); + + $this->cacheFactory = $this->createMock(ICacheFactory::class); + $this->timeFactory = $this->createMock(ITimeFactory::class); + $this->cache = $this->createMock(ICache::class); + + $this->cacheFactory + ->expects($this->once()) + ->method('createDistributed') + ->with('OC\Security\RateLimiting\Backend\MemoryCacheBackend') + ->willReturn($this->cache); + + $this->memoryCache = new MemoryCacheBackend( + $this->cacheFactory, + $this->timeFactory + ); + } + + public function testGetAttemptsWithNoAttemptsBefore() { + $this->cache + ->expects($this->once()) + ->method('get') + ->with('eea460b8d756885099c7f0a4c083bf6a745069ee4a301984e726df58fd4510bffa2dac4b7fd5d835726a6753ffa8343ba31c7e902bbef78fc68c2e743667cb4b') + ->willReturn(null); + + $this->assertSame(0, $this->memoryCache->getAttempts('Method', 'User')); + } + + public function testGetAttempts() { + $this->timeFactory + ->expects($this->once()) + ->method('getTime') + ->willReturn(210); + $this->cache + ->expects($this->once()) + ->method('get') + ->with('eea460b8d756885099c7f0a4c083bf6a745069ee4a301984e726df58fd4510bffa2dac4b7fd5d835726a6753ffa8343ba31c7e902bbef78fc68c2e743667cb4b') + ->willReturn(json_encode([ + '1', + '2', + '87', + '223', + '223', + '224', + ])); + + $this->assertSame(3, $this->memoryCache->getAttempts('Method', 'User')); + } + + public function testRegisterAttemptWithNoAttemptsBefore() { + $this->timeFactory + ->expects($this->once()) + ->method('getTime') + ->willReturn(123); + + $this->cache + ->expects($this->once()) + ->method('get') + ->with('eea460b8d756885099c7f0a4c083bf6a745069ee4a301984e726df58fd4510bffa2dac4b7fd5d835726a6753ffa8343ba31c7e902bbef78fc68c2e743667cb4b') + ->willReturn(null); + $this->cache + ->expects($this->once()) + ->method('set') + ->with( + 'eea460b8d756885099c7f0a4c083bf6a745069ee4a301984e726df58fd4510bffa2dac4b7fd5d835726a6753ffa8343ba31c7e902bbef78fc68c2e743667cb4b', + json_encode(['223']) + ); + + $this->memoryCache->registerAttempt('Method', 'User', 100); + } + + public function testRegisterAttempt() { + $this->timeFactory + ->expects($this->once()) + ->method('getTime') + ->willReturn(86); + + $this->cache + ->expects($this->once()) + ->method('get') + ->with('eea460b8d756885099c7f0a4c083bf6a745069ee4a301984e726df58fd4510bffa2dac4b7fd5d835726a6753ffa8343ba31c7e902bbef78fc68c2e743667cb4b') + ->willReturn(json_encode([ + '1', + '2', + '87', + '123', + '123', + '124', + ])); + $this->cache + ->expects($this->once()) + ->method('set') + ->with( + 'eea460b8d756885099c7f0a4c083bf6a745069ee4a301984e726df58fd4510bffa2dac4b7fd5d835726a6753ffa8343ba31c7e902bbef78fc68c2e743667cb4b', + json_encode([ + '87', + '123', + '123', + '124', + '186', + ]) + ); + + $this->memoryCache->registerAttempt('Method', 'User', 100); + } +} diff --git a/tests/lib/Security/RateLimiting/Backend/MemoryCacheTest.php b/tests/lib/Security/RateLimiting/Backend/MemoryCacheTest.php deleted file mode 100644 index 902c586dc13..00000000000 --- a/tests/lib/Security/RateLimiting/Backend/MemoryCacheTest.php +++ /dev/null @@ -1,146 +0,0 @@ - - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - * - */ - -namespace Test\Security\RateLimiting\Backend; - -use OC\Security\RateLimiting\Backend\MemoryCache; -use OCP\AppFramework\Utility\ITimeFactory; -use OCP\ICache; -use OCP\ICacheFactory; -use Test\TestCase; - -class MemoryCacheTest extends TestCase { - /** @var ICacheFactory|\PHPUnit\Framework\MockObject\MockObject */ - private $cacheFactory; - /** @var ITimeFactory|\PHPUnit\Framework\MockObject\MockObject */ - private $timeFactory; - /** @var ICache|\PHPUnit\Framework\MockObject\MockObject */ - private $cache; - /** @var MemoryCache */ - private $memoryCache; - - protected function setUp(): void { - parent::setUp(); - - $this->cacheFactory = $this->createMock(ICacheFactory::class); - $this->timeFactory = $this->createMock(ITimeFactory::class); - $this->cache = $this->createMock(ICache::class); - - $this->cacheFactory - ->expects($this->once()) - ->method('createDistributed') - ->with('OC\Security\RateLimiting\Backend\MemoryCache') - ->willReturn($this->cache); - - $this->memoryCache = new MemoryCache( - $this->cacheFactory, - $this->timeFactory - ); - } - - public function testGetAttemptsWithNoAttemptsBefore() { - $this->cache - ->expects($this->once()) - ->method('get') - ->with('eea460b8d756885099c7f0a4c083bf6a745069ee4a301984e726df58fd4510bffa2dac4b7fd5d835726a6753ffa8343ba31c7e902bbef78fc68c2e743667cb4b') - ->willReturn(null); - - $this->assertSame(0, $this->memoryCache->getAttempts('Method', 'User', 123)); - } - - public function testGetAttempts() { - $this->timeFactory - ->expects($this->once()) - ->method('getTime') - ->willReturn(210); - $this->cache - ->expects($this->once()) - ->method('get') - ->with('eea460b8d756885099c7f0a4c083bf6a745069ee4a301984e726df58fd4510bffa2dac4b7fd5d835726a6753ffa8343ba31c7e902bbef78fc68c2e743667cb4b') - ->willReturn(json_encode([ - '1', - '2', - '87', - '123', - '123', - '124', - ])); - - $this->assertSame(3, $this->memoryCache->getAttempts('Method', 'User', 123)); - } - - public function testRegisterAttemptWithNoAttemptsBefore() { - $this->timeFactory - ->expects($this->once()) - ->method('getTime') - ->willReturn(123); - - $this->cache - ->expects($this->once()) - ->method('get') - ->with('eea460b8d756885099c7f0a4c083bf6a745069ee4a301984e726df58fd4510bffa2dac4b7fd5d835726a6753ffa8343ba31c7e902bbef78fc68c2e743667cb4b') - ->willReturn(null); - $this->cache - ->expects($this->once()) - ->method('set') - ->with( - 'eea460b8d756885099c7f0a4c083bf6a745069ee4a301984e726df58fd4510bffa2dac4b7fd5d835726a6753ffa8343ba31c7e902bbef78fc68c2e743667cb4b', - json_encode(['123']) - ); - - $this->memoryCache->registerAttempt('Method', 'User', 100); - } - - public function testRegisterAttempt() { - $this->timeFactory - ->expects($this->once()) - ->method('getTime') - ->willReturn(129); - - $this->cache - ->expects($this->once()) - ->method('get') - ->with('eea460b8d756885099c7f0a4c083bf6a745069ee4a301984e726df58fd4510bffa2dac4b7fd5d835726a6753ffa8343ba31c7e902bbef78fc68c2e743667cb4b') - ->willReturn(json_encode([ - '1', - '2', - '87', - '123', - '123', - '124', - ])); - $this->cache - ->expects($this->once()) - ->method('set') - ->with( - 'eea460b8d756885099c7f0a4c083bf6a745069ee4a301984e726df58fd4510bffa2dac4b7fd5d835726a6753ffa8343ba31c7e902bbef78fc68c2e743667cb4b', - json_encode([ - '87', - '123', - '123', - '124', - '129', - ]) - ); - - $this->memoryCache->registerAttempt('Method', 'User', 100); - } -} diff --git a/tests/lib/Security/RateLimiting/LimiterTest.php b/tests/lib/Security/RateLimiting/LimiterTest.php index 76121a49bc1..319a695a4a7 100644 --- a/tests/lib/Security/RateLimiting/LimiterTest.php +++ b/tests/lib/Security/RateLimiting/LimiterTest.php @@ -23,13 +23,10 @@ namespace Test\Security\RateLimiting; use OC\Security\RateLimiting\Backend\IBackend; use OC\Security\RateLimiting\Limiter; -use OCP\AppFramework\Utility\ITimeFactory; use OCP\IUser; use Test\TestCase; class LimiterTest extends TestCase { - /** @var ITimeFactory|\PHPUnit\Framework\MockObject\MockObject */ - private $timeFactory; /** @var IBackend|\PHPUnit\Framework\MockObject\MockObject */ private $backend; /** @var Limiter */ @@ -38,11 +35,9 @@ class LimiterTest extends TestCase { protected function setUp(): void { parent::setUp(); - $this->timeFactory = $this->createMock(ITimeFactory::class); $this->backend = $this->createMock(IBackend::class); $this->limiter = new Limiter( - $this->timeFactory, $this->backend ); } @@ -57,8 +52,7 @@ class LimiterTest extends TestCase { ->method('getAttempts') ->with( 'MyIdentifier', - '4664f0d9c88dcb7552be47b37bb52ce35977b2e60e1ac13757cf625f31f87050a41f3da064887fa87d49fd042e4c8eb20de8f10464877d3959677ab011b73a47', - 100 + '4664f0d9c88dcb7552be47b37bb52ce35977b2e60e1ac13757cf625f31f87050a41f3da064887fa87d49fd042e4c8eb20de8f10464877d3959677ab011b73a47' ) ->willReturn(101); @@ -66,17 +60,12 @@ class LimiterTest extends TestCase { } public function testRegisterAnonRequestSuccess() { - $this->timeFactory - ->expects($this->once()) - ->method('getTime') - ->willReturn(2000); $this->backend ->expects($this->once()) ->method('getAttempts') ->with( 'MyIdentifier', - '4664f0d9c88dcb7552be47b37bb52ce35977b2e60e1ac13757cf625f31f87050a41f3da064887fa87d49fd042e4c8eb20de8f10464877d3959677ab011b73a47', - 100 + '4664f0d9c88dcb7552be47b37bb52ce35977b2e60e1ac13757cf625f31f87050a41f3da064887fa87d49fd042e4c8eb20de8f10464877d3959677ab011b73a47' ) ->willReturn(99); $this->backend @@ -85,7 +74,7 @@ class LimiterTest extends TestCase { ->with( 'MyIdentifier', '4664f0d9c88dcb7552be47b37bb52ce35977b2e60e1ac13757cf625f31f87050a41f3da064887fa87d49fd042e4c8eb20de8f10464877d3959677ab011b73a47', - 2000 + 100 ); $this->limiter->registerAnonRequest('MyIdentifier', 100, 100, '127.0.0.1'); @@ -107,8 +96,7 @@ class LimiterTest extends TestCase { ->method('getAttempts') ->with( 'MyIdentifier', - 'ddb2ec50fa973fd49ecf3d816f677c8095143e944ad10485f30fb3dac85c13a346dace4dae2d0a15af91867320957bfd38a43d9eefbb74fe6919e15119b6d805', - 100 + 'ddb2ec50fa973fd49ecf3d816f677c8095143e944ad10485f30fb3dac85c13a346dace4dae2d0a15af91867320957bfd38a43d9eefbb74fe6919e15119b6d805' ) ->willReturn(101); @@ -123,17 +111,12 @@ class LimiterTest extends TestCase { ->method('getUID') ->willReturn('MyUid'); - $this->timeFactory - ->expects($this->once()) - ->method('getTime') - ->willReturn(2000); $this->backend ->expects($this->once()) ->method('getAttempts') ->with( 'MyIdentifier', - 'ddb2ec50fa973fd49ecf3d816f677c8095143e944ad10485f30fb3dac85c13a346dace4dae2d0a15af91867320957bfd38a43d9eefbb74fe6919e15119b6d805', - 100 + 'ddb2ec50fa973fd49ecf3d816f677c8095143e944ad10485f30fb3dac85c13a346dace4dae2d0a15af91867320957bfd38a43d9eefbb74fe6919e15119b6d805' ) ->willReturn(99); $this->backend @@ -142,7 +125,7 @@ class LimiterTest extends TestCase { ->with( 'MyIdentifier', 'ddb2ec50fa973fd49ecf3d816f677c8095143e944ad10485f30fb3dac85c13a346dace4dae2d0a15af91867320957bfd38a43d9eefbb74fe6919e15119b6d805', - 2000 + 100 ); $this->limiter->registerUserRequest('MyIdentifier', 100, 100, $user); diff --git a/version.php b/version.php index 3491abb41e0..965eb355e5b 100644 --- a/version.php +++ b/version.php @@ -30,7 +30,7 @@ // between betas, final and RCs. This is _not_ the public version number. Reset minor/patchlevel // when updating major/minor version number. -$OC_Version = [21, 0, 4, 1]; +$OC_Version = [21, 0, 4, 2]; // The human readable string $OC_VersionString = '21.0.4'; -- cgit v1.2.3