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 3 years ago
parent 1895a6dc57
commit b578a1e8b5
No known key found for this signature in database
GPG Key ID: 7076EA9751AACDDA

@ -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>

@ -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

@ -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']);

@ -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.'));
}

@ -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;
}

@ -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;
}

@ -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());
}
}

@ -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