Browse Source

Fair use of push notifications

We want to keep offering our push notification service for free, but large
users overload our infrastructure. For this reason we have to rate-limit the
use of push notifications. If you need this feature, consider setting up your
own push server or using Nextcloud Enterprise.

Signed-off-by: Joas Schilling <coding@schilljs.com>
pull/29363/head
Joas Schilling 5 years ago
parent
commit
b578a1e8b5
No known key found for this signature in database GPG Key ID: 7076EA9751AACDDA
  1. 1
      build/psalm-baseline.xml
  2. 80
      lib/private/Notification/Manager.php
  3. 23
      lib/private/Support/Subscription/Registry.php
  4. 7
      lib/private/User/Manager.php
  5. 12
      lib/public/Notification/IManager.php
  6. 4
      lib/public/Support/Subscription/IRegistry.php
  7. 86
      tests/lib/Notification/ManagerTest.php
  8. 9
      tests/lib/Support/Subscription/RegistryTest.php

1
build/psalm-baseline.xml

@ -4253,6 +4253,7 @@
</ParamNameMismatch>
</file>
<file src="lib/private/Notification/Manager.php">
<InvalidCatch occurrences="3"/>
<TypeDoesNotContainType occurrences="2">
<code>!($notification instanceof INotification)</code>
<code>!($notification instanceof INotification)</code>

80
lib/private/Notification/Manager.php

@ -27,8 +27,10 @@ declare(strict_types=1);
namespace OC\Notification;
use OC\AppFramework\Bootstrap\Coordinator;
use OCP\AppFramework\QueryException;
use OCP\ILogger;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\ICache;
use OCP\ICacheFactory;
use OCP\IUserManager;
use OCP\Notification\AlreadyProcessedException;
use OCP\Notification\IApp;
use OCP\Notification\IDeferrableApp;
@ -37,11 +39,22 @@ use OCP\Notification\IManager;
use OCP\Notification\INotification;
use OCP\Notification\INotifier;
use OCP\RichObjectStrings\IValidator;
use OCP\Support\Subscription\IRegistry;
use Psr\Container\ContainerExceptionInterface;
use Psr\Log\LoggerInterface;
class Manager implements IManager {
/** @var IValidator */
protected $validator;
/** @var ILogger */
/** @var IUserManager */
private $userManager;
/** @var ICache */
protected $cache;
/** @var ITimeFactory */
protected $timeFactory;
/** @var IRegistry */
protected $subscription;
/** @var LoggerInterface */
protected $logger;
/** @var Coordinator */
private $coordinator;
@ -64,9 +77,17 @@ class Manager implements IManager {
private $parsedRegistrationContext;
public function __construct(IValidator $validator,
ILogger $logger,
IUserManager $userManager,
ICacheFactory $cacheFactory,
ITimeFactory $timeFactory,
IRegistry $subscription,
LoggerInterface $logger,
Coordinator $coordinator) {
$this->validator = $validator;
$this->userManager = $userManager;
$this->cache = $cacheFactory->createDistributed('notifications');
$this->timeFactory = $timeFactory;
$this->subscription = $subscription;
$this->logger = $logger;
$this->coordinator = $coordinator;
@ -97,9 +118,10 @@ class Manager implements IManager {
*/
public function registerNotifier(\Closure $service, \Closure $info) {
$infoData = $info();
$this->logger->logException(new \InvalidArgumentException(
$exception = new \InvalidArgumentException(
'Notifier ' . $infoData['name'] . ' (id: ' . $infoData['id'] . ') is not considered because it is using the old way to register.'
));
);
$this->logger->error($exception->getMessage(), ['exception' => $exception]);
}
/**
@ -121,10 +143,10 @@ class Manager implements IManager {
foreach ($this->appClasses as $appClass) {
try {
$app = \OC::$server->query($appClass);
} catch (QueryException $e) {
$this->logger->logException($e, [
'message' => 'Failed to load notification app class: ' . $appClass,
$app = \OC::$server->get($appClass);
} catch (ContainerExceptionInterface $e) {
$this->logger->error('Failed to load notification app class: ' . $appClass, [
'exception' => $e,
'app' => 'notifications',
]);
continue;
@ -153,10 +175,10 @@ class Manager implements IManager {
$notifierServices = $this->coordinator->getRegistrationContext()->getNotifierServices();
foreach ($notifierServices as $notifierService) {
try {
$notifier = \OC::$server->query($notifierService->getService());
} catch (QueryException $e) {
$this->logger->logException($e, [
'message' => 'Failed to load notification notifier class: ' . $notifierService->getService(),
$notifier = \OC::$server->get($notifierService->getService());
} catch (ContainerExceptionInterface $e) {
$this->logger->error('Failed to load notification notifier class: ' . $notifierService->getService(), [
'exception' => $e,
'app' => 'notifications',
]);
continue;
@ -181,10 +203,10 @@ class Manager implements IManager {
foreach ($this->notifierClasses as $notifierClass) {
try {
$notifier = \OC::$server->query($notifierClass);
} catch (QueryException $e) {
$this->logger->logException($e, [
'message' => 'Failed to load notification notifier class: ' . $notifierClass,
$notifier = \OC::$server->get($notifierClass);
} catch (ContainerExceptionInterface $e) {
$this->logger->error('Failed to load notification notifier class: ' . $notifierClass, [
'exception' => $e,
'app' => 'notifications',
]);
continue;
@ -277,6 +299,28 @@ class Manager implements IManager {
$this->deferPushing = false;
}
/**
* {@inheritDoc}
*/
public function isFairUseOfFreePushService(): bool {
$pushAllowed = $this->cache->get('push_fair_use');
if ($pushAllowed === null) {
/**
* We want to keep offering our push notification service for free, but large
* users overload our infrastructure. For this reason we have to rate-limit the
* use of push notifications. If you need this feature, consider setting up your
* own push server or using Nextcloud Enterprise.
*/
// TODO Remove time check after 1st March 2022
$isFairUse = $this->timeFactory->getTime() < 1646089200
|| $this->subscription->delegateHasValidSubscription()
|| $this->userManager->countSeenUsers() < 5000;
$pushAllowed = $isFairUse ? 'yes' : 'no';
$this->cache->set('push_fair_use', $pushAllowed, 3600);
}
return $pushAllowed === 'yes';
}
/**
* @param INotification $notification
* @throws \InvalidArgumentException When the notification is not valid

23
lib/private/Support/Subscription/Registry.php

@ -59,21 +59,17 @@ class Registry implements IRegistry {
private $groupManager;
/** @var LoggerInterface */
private $logger;
/** @var IManager */
private $notificationManager;
public function __construct(IConfig $config,
IServerContainer $container,
IUserManager $userManager,
IGroupManager $groupManager,
LoggerInterface $logger,
IManager $notificationManager) {
LoggerInterface $logger) {
$this->config = $config;
$this->container = $container;
$this->userManager = $userManager;
$this->groupManager = $groupManager;
$this->logger = $logger;
$this->notificationManager = $notificationManager;
}
private function getSubscription(): ?ISubscription {
@ -158,15 +154,16 @@ class Registry implements IRegistry {
/**
* Indicates if a hard user limit is reached and no new users should be created
*
* @param IManager|null $notificationManager
* @since 21.0.0
*/
public function delegateIsHardUserLimitReached(): bool {
public function delegateIsHardUserLimitReached(?IManager $notificationManager = null): bool {
$subscription = $this->getSubscription();
if ($subscription instanceof ISubscription &&
$subscription->hasValidSubscription()) {
$userLimitReached = $subscription->isHardUserLimitReached();
if ($userLimitReached) {
$this->notifyAboutReachedUserLimit();
if ($userLimitReached && $notificationManager instanceof IManager) {
$this->notifyAboutReachedUserLimit($notificationManager);
}
return $userLimitReached;
}
@ -181,8 +178,8 @@ class Registry implements IRegistry {
$hardUserLimit = $this->config->getSystemValue('one-click-instance.user-limit', 50);
$userLimitReached = $userCount >= $hardUserLimit;
if ($userLimitReached) {
$this->notifyAboutReachedUserLimit();
if ($userLimitReached && $notificationManager instanceof IManager) {
$this->notifyAboutReachedUserLimit($notificationManager);
}
return $userLimitReached;
}
@ -216,17 +213,17 @@ class Registry implements IRegistry {
return $userCount;
}
private function notifyAboutReachedUserLimit() {
private function notifyAboutReachedUserLimit(IManager $notificationManager) {
$admins = $this->groupManager->get('admin')->getUsers();
foreach ($admins as $admin) {
$notification = $this->notificationManager->createNotification();
$notification = $notificationManager->createNotification();
$notification->setApp('core')
->setUser($admin->getUID())
->setDateTime(new \DateTime())
->setObject('user_limit_reached', '1')
->setSubject('user_limit_reached');
$this->notificationManager->notify($notification);
$notificationManager->notify($notification);
}
$this->logger->warning('The user limit was reached and the new user was not created', ['app' => 'lib']);

7
lib/private/User/Manager.php

@ -44,6 +44,7 @@ use OCP\IGroup;
use OCP\IUser;
use OCP\IUserBackend;
use OCP\IUserManager;
use OCP\Notification\IManager;
use OCP\Support\Subscription\IRegistry;
use OCP\User\Backend\IGetRealUIDBackend;
use OCP\User\Backend\ISearchKnownUsersBackend;
@ -379,7 +380,11 @@ class Manager extends PublicEmitter implements IUserManager {
*/
public function createUser($uid, $password) {
// DI injection is not used here as IRegistry needs the user manager itself for user count and thus it would create a cyclic dependency
if (\OC::$server->get(IRegistry::class)->delegateIsHardUserLimitReached()) {
/** @var IRegistry $registry */
$registry = \OC::$server->get(IRegistry::class);
/** @var IManager $notificationManager */
$notificationManager = \OC::$server->get(IManager::class);
if ($registry->delegateIsHardUserLimitReached($notificationManager)) {
$l = \OC::$server->getL10N('lib');
throw new HintException($l->t('The user limit has been reached and the user was not created.'));
}

12
lib/public/Notification/IManager.php

@ -107,4 +107,16 @@ interface IManager extends IApp, INotifier {
* @since 20.0.0
*/
public function flush(): void;
/**
* Whether the server can use the hosted push notification service
*
* We want to keep offering our push notification service for free, but large
* users overload our infrastructure. For this reason we have to rate-limit the
* use of push notifications. If you need this feature, consider setting up your
* own push server or using Nextcloud Enterprise.
*
* @since 23.0.0
*/
public function isFairUseOfFreePushService(): bool;
}

4
lib/public/Support/Subscription/IRegistry.php

@ -27,6 +27,7 @@ declare(strict_types=1);
*/
namespace OCP\Support\Subscription;
use OCP\Notification\IManager;
use OCP\Support\Subscription\Exception\AlreadyRegisteredException;
/**
@ -81,7 +82,8 @@ interface IRegistry {
/**
* Indicates if a hard user limit is reached and no new users should be created
*
* @param IManager|null $notificationManager
* @since 21.0.0
*/
public function delegateIsHardUserLimitReached(): bool;
public function delegateIsHardUserLimitReached(?IManager $notificationManager = null): bool;
}

86
tests/lib/Notification/ManagerTest.php

@ -1,4 +1,6 @@
<?php
declare(strict_types=1);
/**
* @author Joas Schilling <nickvergessen@owncloud.com>
*
@ -25,11 +27,16 @@ use OC\AppFramework\Bootstrap\Coordinator;
use OC\AppFramework\Bootstrap\RegistrationContext;
use OC\AppFramework\Bootstrap\ServiceRegistration;
use OC\Notification\Manager;
use OCP\ILogger;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\ICache;
use OCP\ICacheFactory;
use OCP\IUserManager;
use OCP\Notification\IManager;
use OCP\Notification\INotification;
use OCP\RichObjectStrings\IValidator;
use OCP\Support\Subscription\IRegistry;
use PHPUnit\Framework\MockObject\MockObject;
use Psr\Log\LoggerInterface;
use Test\TestCase;
class ManagerTest extends TestCase {
@ -38,7 +45,17 @@ class ManagerTest extends TestCase {
/** @var IValidator|MockObject */
protected $validator;
/** @var ILogger|MockObject */
/** @var IUserManager|MockObject */
protected $userManager;
/** @var ICacheFactory|MockObject */
protected $cacheFactory;
/** @var ICache|MockObject */
protected $cache;
/** @var ITimeFactory|MockObject */
protected $timeFactory;
/** @var IRegistry|MockObject */
protected $subscriptionRegistry;
/** @var LoggerInterface|MockObject */
protected $logger;
/** @var Coordinator|MockObject */
protected $coordinator;
@ -49,14 +66,23 @@ class ManagerTest extends TestCase {
parent::setUp();
$this->validator = $this->createMock(IValidator::class);
$this->logger = $this->createMock(ILogger::class);
$this->userManager = $this->createMock(IUserManager::class);
$this->cache = $this->createMock(ICache::class);
$this->timeFactory = $this->createMock(ITimeFactory::class);
$this->subscriptionRegistry = $this->createMock(IRegistry::class);
$this->logger = $this->createMock(LoggerInterface::class);
$this->cacheFactory = $this->createMock(ICacheFactory::class);
$this->cacheFactory->method('createDistributed')
->with('notifications')
->willReturn($this->cache);
$this->registrationContext = $this->createMock(RegistrationContext::class);
$this->coordinator = $this->createMock(Coordinator::class);
$this->coordinator->method('getRegistrationContext')
->willReturn($this->registrationContext);
$this->manager = new Manager($this->validator, $this->logger, $this->coordinator);
$this->manager = new Manager($this->validator, $this->userManager, $this->cacheFactory, $this->timeFactory, $this->subscriptionRegistry, $this->logger, $this->coordinator);
}
public function testRegisterApp() {
@ -128,6 +154,10 @@ class ManagerTest extends TestCase {
$manager = $this->getMockBuilder(Manager::class)
->setConstructorArgs([
$this->validator,
$this->userManager,
$this->cacheFactory,
$this->timeFactory,
$this->subscriptionRegistry,
$this->logger,
$this->coordinator,
])
@ -156,6 +186,10 @@ class ManagerTest extends TestCase {
$manager = $this->getMockBuilder(Manager::class)
->setConstructorArgs([
$this->validator,
$this->userManager,
$this->cacheFactory,
$this->timeFactory,
$this->subscriptionRegistry,
$this->logger,
$this->coordinator,
])
@ -177,6 +211,10 @@ class ManagerTest extends TestCase {
$manager = $this->getMockBuilder(Manager::class)
->setConstructorArgs([
$this->validator,
$this->userManager,
$this->cacheFactory,
$this->timeFactory,
$this->subscriptionRegistry,
$this->logger,
$this->coordinator,
])
@ -199,6 +237,10 @@ class ManagerTest extends TestCase {
$manager = $this->getMockBuilder(Manager::class)
->setConstructorArgs([
$this->validator,
$this->userManager,
$this->cacheFactory,
$this->timeFactory,
$this->subscriptionRegistry,
$this->logger,
$this->coordinator,
])
@ -211,4 +253,40 @@ class ManagerTest extends TestCase {
$manager->getCount($notification);
}
public function dataIsFairUseOfFreePushService() {
return [
// Before 1st March
[1646089199, true, 4999, true],
[1646089199, true, 5000, true],
[1646089199, false, 4999, true],
[1646089199, false, 5000, true],
// After 1st March
[1646089200, true, 4999, true],
[1646089200, true, 5000, true],
[1646089200, false, 4999, true],
[1646089200, false, 5000, false],
];
}
/**
* @dataProvider dataIsFairUseOfFreePushService
* @param int $time
* @param bool $hasValidSubscription
* @param int $userCount
* @param bool $isFair
*/
public function testIsFairUseOfFreePushService(int $time, bool $hasValidSubscription, int $userCount, bool $isFair): void {
$this->timeFactory->method('getTime')
->willReturn($time);
$this->subscriptionRegistry->method('delegateHasValidSubscription')
->willReturn($hasValidSubscription);
$this->userManager->method('countSeenUsers')
->willReturn($userCount);
$this->assertSame($isFair, $this->manager->isFairUseOfFreePushService());
}
}

9
tests/lib/Support/Subscription/RegistryTest.php

@ -75,8 +75,7 @@ class RegistryTest extends TestCase {
$this->serverContainer,
$this->userManager,
$this->groupManager,
$this->logger,
$this->notificationManager
$this->logger
);
}
@ -177,7 +176,7 @@ class RegistryTest extends TestCase {
->method('get')
->willReturn($dummyGroup);
$this->assertSame(true, $this->registry->delegateIsHardUserLimitReached());
$this->assertSame(true, $this->registry->delegateIsHardUserLimitReached($this->notificationManager));
}
public function testDelegateIsHardUserLimitReachedWithoutSupportApp() {
@ -186,7 +185,7 @@ class RegistryTest extends TestCase {
->with('one-click-instance')
->willReturn(false);
$this->assertSame(false, $this->registry->delegateIsHardUserLimitReached());
$this->assertSame(false, $this->registry->delegateIsHardUserLimitReached($this->notificationManager));
}
public function dataForUserLimitCheck() {
@ -237,6 +236,6 @@ class RegistryTest extends TestCase {
->willReturn($dummyGroup);
}
$this->assertSame($expectedResult, $this->registry->delegateIsHardUserLimitReached());
$this->assertSame($expectedResult, $this->registry->delegateIsHardUserLimitReached($this->notificationManager));
}
}
Loading…
Cancel
Save