-
-
Notifications
You must be signed in to change notification settings - Fork 4.2k
Commit
Signed-off-by: Christoph Wurst <christoph@winzerhof-wurst.at>
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,111 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
/* | ||
* @copyright 2023 Christoph Wurst <christoph@winzerhof-wurst.at> | ||
* | ||
* @author 2023 Christoph Wurst <christoph@winzerhof-wurst.at> | ||
* | ||
* @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 <http://www.gnu.org/licenses/>. | ||
*/ | ||
|
||
namespace OCA\DAV\CalDAV\Security; | ||
|
||
use OC\Security\RateLimiting\Exception\RateLimitExceededException; | ||
use OC\Security\RateLimiting\Limiter; | ||
use OCA\DAV\CalDAV\CalDavBackend; | ||
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 RateLimitingPlugin extends ServerPlugin { | ||
|
||
private Limiter $limiter; | ||
private IUserManager $userManager; | ||
private CalDavBackend $calDavBackend; | ||
private IAppConfig $config; | ||
private LoggerInterface $logger; | ||
private ?string $userId; | ||
|
||
public function __construct(Limiter $limiter, | ||
IUserManager $userManager, | ||
CalDavBackend $calDavBackend, | ||
LoggerInterface $logger, | ||
IAppConfig $config, | ||
?string $userId) { | ||
$this->limiter = $limiter; | ||
$this->userManager = $userManager; | ||
$this->calDavBackend = $calDavBackend; | ||
$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) === 3 && $pathParts[0] === 'calendars') { | ||
// Path looks like calendars/username/calendarname so a new calendar or subscription is created | ||
try { | ||
$this->limiter->registerUserRequest( | ||
'caldav-create-calendar', | ||
$this->config->getValueInt('dav', 'rateLimitCalendarCreation', 10), | ||
Check failure Code scanning / Psalm UndefinedInterfaceMethod Error
Method OCP\IAppConfig::getValueInt does not exist
Check failure on line 85 in apps/dav/lib/CalDAV/Security/RateLimitingPlugin.php GitHub Actions / static-code-analysisUndefinedInterfaceMethod
|
||
$this->config->getValueInt('dav', 'rateLimitPeriodCalendarCreation', 3600), | ||
Check failure Code scanning / Psalm UndefinedInterfaceMethod Error
Method OCP\IAppConfig::getValueInt does not exist
Check failure on line 86 in apps/dav/lib/CalDAV/Security/RateLimitingPlugin.php GitHub Actions / static-code-analysisUndefinedInterfaceMethod
|
||
$user | ||
); | ||
} catch (RateLimitExceededException $e) { | ||
throw new TooManyRequests('Too many calendars created', 0, $e); | ||
} | ||
|
||
$calendarLimit = $this->config->getValueInt('dav', 'maximumCalendarsSubscriptions', 30); | ||
Check failure Code scanning / Psalm UndefinedInterfaceMethod Error
Method OCP\IAppConfig::getValueInt does not exist
Check failure on line 93 in apps/dav/lib/CalDAV/Security/RateLimitingPlugin.php GitHub Actions / static-code-analysisUndefinedInterfaceMethod
|
||
if ($calendarLimit === -1) { | ||
return; | ||
} | ||
$numCalendars = $this->calDavBackend->getCalendarsForUserCount('principals/users/' . $user->getUID()); | ||
$numSubscriptions = $this->calDavBackend->getSubscriptionsForUserCount('principals/users/' . $user->getUID()); | ||
|
||
if (($numCalendars + $numSubscriptions) >= $calendarLimit) { | ||
$this->logger->warning('Maximum number of calendars/subscriptions reached', [ | ||
'calendars' => $numCalendars, | ||
'subscription' => $numSubscriptions, | ||
'limit' => $calendarLimit, | ||
]); | ||
throw new Forbidden('Calendar limit reached', 0); | ||
} | ||
} | ||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,166 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
/* | ||
* @copyright 2023 Christoph Wurst <christoph@winzerhof-wurst.at> | ||
* | ||
* @author 2023 Christoph Wurst <christoph@winzerhof-wurst.at> | ||
* | ||
* @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 <http://www.gnu.org/licenses/>. | ||
*/ | ||
|
||
namespace OCA\DAV\Tests\unit\CalDAV\Security; | ||
|
||
use OC\Security\RateLimiting\Exception\RateLimitExceededException; | ||
use OC\Security\RateLimiting\Limiter; | ||
use OCA\DAV\CalDAV\CalDavBackend; | ||
use OCA\DAV\CalDAV\Security\RateLimitingPlugin; | ||
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 RateLimitingPluginTest extends TestCase { | ||
|
||
private Limiter|MockObject $limiter; | ||
private CalDavBackend|MockObject $caldavBackend; | ||
private IUserManager|MockObject $userManager; | ||
private LoggerInterface|MockObject $logger; | ||
private IAppConfig|MockObject $config; | ||
private string $userId = 'user123'; | ||
private RateLimitingPlugin $plugin; | ||
|
||
protected function setUp(): void { | ||
parent::setUp(); | ||
|
||
$this->limiter = $this->createMock(Limiter::class); | ||
$this->userManager = $this->createMock(IUserManager::class); | ||
$this->caldavBackend = $this->createMock(CalDavBackend::class); | ||
$this->logger = $this->createMock(LoggerInterface::class); | ||
$this->config = $this->createMock(IAppConfig::class); | ||
$this->plugin = new RateLimitingPlugin( | ||
$this->limiter, | ||
$this->userManager, | ||
$this->caldavBackend, | ||
$this->logger, | ||
$this->config, | ||
$this->userId, | ||
); | ||
} | ||
|
||
public function testNoUserObject(): void { | ||
$this->limiter->expects(self::never()) | ||
->method('registerUserRequest'); | ||
|
||
$this->plugin->beforeBind('calendars/foo/cal'); | ||
} | ||
|
||
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 testRegisterCalendarCreation(): 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( | ||
'caldav-create-calendar', | ||
10, | ||
3600, | ||
$user, | ||
); | ||
|
||
$this->plugin->beforeBind('calendars/foo/cal'); | ||
} | ||
|
||
public function testCalendarCreationRateLimitExceeded(): 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( | ||
'caldav-create-calendar', | ||
10, | ||
3600, | ||
$user, | ||
) | ||
->willThrowException(new RateLimitExceededException()); | ||
$this->expectException(TooManyRequests::class); | ||
|
||
$this->plugin->beforeBind('calendars/foo/cal'); | ||
} | ||
|
||
public function testCalendarLimitReached(): 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( | ||
'caldav-create-calendar', | ||
10, | ||
3600, | ||
$user, | ||
); | ||
$this->caldavBackend->expects(self::once()) | ||
->method('getCalendarsForUserCount') | ||
->with('principals/users/user123') | ||
->willReturn(27); | ||
$this->caldavBackend->expects(self::once()) | ||
->method('getSubscriptionsForUserCount') | ||
->with('principals/users/user123') | ||
->willReturn(3); | ||
$this->expectException(Forbidden::class); | ||
|
||
$this->plugin->beforeBind('calendars/foo/cal'); | ||
} | ||
|
||
} |