Skip to content

Commit

Permalink
fix(dav): Rate limit address book creation
Browse files Browse the repository at this point in the history
Signed-off-by: Hamza Mahjoubi <[email protected]>
  • Loading branch information
hamza221 committed May 27, 2024
1 parent d87c232 commit fe78094
Show file tree
Hide file tree
Showing 6 changed files with 240 additions and 0 deletions.
2 changes: 2 additions & 0 deletions apps/dav/appinfo/v1/carddav.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
use OCA\DAV\AppInfo\PluginManager;
use OCA\DAV\CardDAV\AddressBookRoot;
use OCA\DAV\CardDAV\CardDavBackend;
use OCA\DAV\CardDAV\Security\CardDavRateLimitingPlugin;
use OCA\DAV\Connector\LegacyDAVACL;
use OCA\DAV\Connector\Sabre\Auth;
use OCA\DAV\Connector\Sabre\ExceptionLoggerPlugin;
Expand Down Expand Up @@ -109,6 +110,7 @@
\OC::$server->get(LoggerInterface::class)
)));
$server->addPlugin(new ExceptionLoggerPlugin('carddav', \OC::$server->get(LoggerInterface::class)));
$server->addPlugin(\OCP\Server::get(CardDavRateLimitingPlugin::class));

// And off we go!
$server->exec();
1 change: 1 addition & 0 deletions apps/dav/composer/composer/autoload_classmap.php
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@
'OCA\\DAV\\CardDAV\\MultiGetExportPlugin' => $baseDir . '/../lib/CardDAV/MultiGetExportPlugin.php',
'OCA\\DAV\\CardDAV\\PhotoCache' => $baseDir . '/../lib/CardDAV/PhotoCache.php',
'OCA\\DAV\\CardDAV\\Plugin' => $baseDir . '/../lib/CardDAV/Plugin.php',
'OCA\\DAV\\CardDAV\\Security\\CardDavRateLimitingPlugin' => $baseDir . '/../lib/CardDAV/Security/CardDavRateLimitingPlugin.php',
'OCA\\DAV\\CardDAV\\Sharing\\Backend' => $baseDir . '/../lib/CardDAV/Sharing/Backend.php',
'OCA\\DAV\\CardDAV\\Sharing\\Service' => $baseDir . '/../lib/CardDAV/Sharing/Service.php',
'OCA\\DAV\\CardDAV\\SyncService' => $baseDir . '/../lib/CardDAV/SyncService.php',
Expand Down
1 change: 1 addition & 0 deletions apps/dav/composer/composer/autoload_static.php
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ class ComposerStaticInitDAV
'OCA\\DAV\\CardDAV\\SystemAddressbook' => __DIR__ . '/..' . '/../lib/CardDAV/SystemAddressbook.php',
'OCA\\DAV\\CardDAV\\UserAddressBooks' => __DIR__ . '/..' . '/../lib/CardDAV/UserAddressBooks.php',
'OCA\\DAV\\CardDAV\\Xml\\Groups' => __DIR__ . '/..' . '/../lib/CardDAV/Xml/Groups.php',
'OCA\\DAV\\CardDAV\\Security\\CardDavRateLimitingPlugin' => __DIR__ . '/..' . '/../lib/CardDAV/Security/CardDavRateLimitingPlugin.php',
'OCA\\DAV\\Command\\CreateAddressBook' => __DIR__ . '/..' . '/../lib/Command/CreateAddressBook.php',
'OCA\\DAV\\Command\\CreateCalendar' => __DIR__ . '/..' . '/../lib/Command/CreateCalendar.php',
'OCA\\DAV\\Command\\DeleteCalendar' => __DIR__ . '/..' . '/../lib/Command/DeleteCalendar.php',
Expand Down
87 changes: 87 additions & 0 deletions apps/dav/lib/CardDAV/Security/CardDavRateLimitingPlugin.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
<?php

declare(strict_types=1);

/*
* SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\DAV\CardDAV\Security;

use OC\Security\RateLimiting\Exception\RateLimitExceededException;
use OC\Security\RateLimiting\Limiter;
use OCA\DAV\CardDAV\CardDavBackend;
use OCA\DAV\Connector\Sabre\Exception\TooManyRequests;
use OCP\IAppConfig;
use OCP\IUserManager;
use Psr\Log\LoggerInterface;
use Sabre\DAV;
use Sabre\DAV\Exception\Forbidden;
use Sabre\DAV\ServerPlugin;
use function count;
use function explode;

class CardDavRateLimitingPlugin extends ServerPlugin {
private ?string $userId;

public function __construct(private Limiter $limiter,
private IUserManager $userManager,
private CardDavBackend $cardDavBackend,
private LoggerInterface $logger,
private IAppConfig $config,
?string $userId) {
$this->limiter = $limiter;
$this->userManager = $userManager;
$this->cardDavBackend = $cardDavBackend;
$this->config = $config;
$this->logger = $logger;
$this->userId = $userId;
}

public function initialize(DAV\Server $server): void {
$server->on('beforeBind', [$this, 'beforeBind'], 1);
}

public function beforeBind(string $path): void {
if ($this->userId === null) {
// We only care about authenticated users here
return;
}
$user = $this->userManager->get($this->userId);
if ($user === null) {
// We only care about authenticated users here
return;
}

$pathParts = explode('/', $path);
if (count($pathParts) === 4 && $pathParts[0] === 'addressbooks') {
// Path looks like addressbooks/users/username/addressbooksname so a new addressbook is created
try {
$this->limiter->registerUserRequest(
'carddav-create-address-book',
$this->config->getValueInt('dav', 'rateLimitAddressBookCreation', 10),
$this->config->getValueInt('dav', 'rateLimitPeriodAddressBookCreation', 3600),
$user
);
} catch (RateLimitExceededException $e) {
throw new TooManyRequests('Too many addressbooks created', 0, $e);
}

$addressBookLimit = $this->config->getValueInt('dav', 'maximumAdressbooks', 10);
if ($addressBookLimit === -1) {
return;
}
$numAddressbooks = $this->cardDavBackend->getAddressBooksForUserCount('principals/users/' . $user->getUID());

if ($numAddressbooks >= $addressBookLimit) {
$this->logger->warning('Maximum number of address books reached', [
'addressbooks' => $numAddressbooks,
'addressBookLimit' => $addressBookLimit,
]);
throw new Forbidden('AddressBook limit reached', 0);
}
}
}

}
3 changes: 3 additions & 0 deletions apps/dav/lib/Server.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
use OCA\DAV\CardDAV\ImageExportPlugin;
use OCA\DAV\CardDAV\MultiGetExportPlugin;
use OCA\DAV\CardDAV\PhotoCache;
use OCA\DAV\CardDAV\Security\CardDavRateLimitingPlugin;
use OCA\DAV\Comments\CommentsPlugin;
use OCA\DAV\Connector\Sabre\AnonymousOptionsPlugin;
use OCA\DAV\Connector\Sabre\Auth;
Expand Down Expand Up @@ -208,6 +209,8 @@ public function __construct(IRequest $request, string $baseUri) {
\OC::$server->getAppDataDir('dav-photocache'),
$logger)
));

$this->server->addPlugin(\OCP\Server::get(CardDavRateLimitingPlugin::class));
}

// system tags plugins
Expand Down
146 changes: 146 additions & 0 deletions apps/dav/tests/unit/CardDAV/Security/CardDavRateLimitingPluginTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
<?php

declare(strict_types=1);

/*
* SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\DAV\Tests\unit\CardDAV\Security;

use OC\Security\RateLimiting\Exception\RateLimitExceededException;
use OC\Security\RateLimiting\Limiter;
use OCA\DAV\CardDAV\CardDavBackend;
use OCA\DAV\CardDAV\Security\CardDavRateLimitingPlugin;
use OCA\DAV\Connector\Sabre\Exception\TooManyRequests;
use OCP\IAppConfig;
use OCP\IUser;
use OCP\IUserManager;
use PHPUnit\Framework\MockObject\MockObject;
use Psr\Log\LoggerInterface;
use Sabre\DAV\Exception\Forbidden;
use Test\TestCase;

class CardDavRateLimitingPluginTest extends TestCase {

private Limiter|MockObject $limiter;
private CardDavBackend|MockObject $cardDavBackend;
private IUserManager|MockObject $userManager;
private LoggerInterface|MockObject $logger;
private IAppConfig|MockObject $config;
private string $userId = 'user123';
private CardDavRateLimitingPlugin $plugin;

protected function setUp(): void {
parent::setUp();

$this->limiter = $this->createMock(Limiter::class);
$this->userManager = $this->createMock(IUserManager::class);
$this->cardDavBackend = $this->createMock(CardDavBackend::class);
$this->logger = $this->createMock(LoggerInterface::class);
$this->config = $this->createMock(IAppConfig::class);
$this->plugin = new CardDavRateLimitingPlugin(
$this->limiter,
$this->userManager,
$this->cardDavBackend,
$this->logger,
$this->config,
$this->userId,
);
}

public function testNoUserObject(): void {
$this->limiter->expects(self::never())
->method('registerUserRequest');

$this->plugin->beforeBind('addressbooks/users/foo/addressbookname');
}

public function testUnrelated(): void {
$user = $this->createMock(IUser::class);
$this->userManager->expects(self::once())
->method('get')
->with($this->userId)
->willReturn($user);
$this->limiter->expects(self::never())
->method('registerUserRequest');

$this->plugin->beforeBind('foo/bar');
}

public function testRegisterAddressBookrCreation(): void {
$user = $this->createMock(IUser::class);
$this->userManager->expects(self::once())
->method('get')
->with($this->userId)
->willReturn($user);
$this->config
->method('getValueInt')
->with('dav')
->willReturnArgument(2);
$this->limiter->expects(self::once())
->method('registerUserRequest')
->with(
'carddav-create-address-book',
10,
3600,
$user,
);

$this->plugin->beforeBind('addressbooks/users/foo/addressbookname');
}

public function testAddressBookCreationRateLimitExceeded(): void {
$user = $this->createMock(IUser::class);
$this->userManager->expects(self::once())
->method('get')
->with($this->userId)
->willReturn($user);
$this->config
->method('getValueInt')
->with('dav')
->willReturnArgument(2);
$this->limiter->expects(self::once())
->method('registerUserRequest')
->with(
'carddav-create-address-book',
10,
3600,
$user,
)
->willThrowException(new RateLimitExceededException());
$this->expectException(TooManyRequests::class);

$this->plugin->beforeBind('addressbooks/users/foo/addressbookname');
}

public function testAddressBookLimitReached(): void {
$user = $this->createMock(IUser::class);
$this->userManager->expects(self::once())
->method('get')
->with($this->userId)
->willReturn($user);
$user->method('getUID')->willReturn('user123');
$this->config
->method('getValueInt')
->with('dav')
->willReturnArgument(2);
$this->limiter->expects(self::once())
->method('registerUserRequest')
->with(
'carddav-create-address-book',
10,
3600,
$user,
);
$this->cardDavBackend->expects(self::once())
->method('getAddressBooksForUserCount')
->with('principals/users/user123')
->willReturn(11);
$this->expectException(Forbidden::class);

$this->plugin->beforeBind('addressbooks/users/foo/addressbookname');
}

}

0 comments on commit fe78094

Please sign in to comment.