From d2fd49c461ed6de7ab6f345ad3afa7c24ca25d90 Mon Sep 17 00:00:00 2001 From: Morris Jobke Date: Thu, 8 Oct 2020 11:41:16 +0200 Subject: [PATCH] Allow subscription to indicate that a userlimit is reached Signed-off-by: Morris Jobke --- build/psalm-baseline.xml | 3 +- lib/private/Support/Subscription/Registry.php | 83 ++++++++++++++++++- lib/private/User/Manager.php | 8 ++ lib/public/Support/Subscription/IRegistry.php | 7 ++ .../Support/Subscription/ISubscription.php | 7 ++ .../Subscription/DummySubscription.php | 9 +- .../lib/Support/Subscription/RegistryTest.php | 83 ++++++++++++++++++- 7 files changed, 194 insertions(+), 6 deletions(-) diff --git a/build/psalm-baseline.xml b/build/psalm-baseline.xml index e5b0f0c77c..d00ea631e5 100644 --- a/build/psalm-baseline.xml +++ b/build/psalm-baseline.xml @@ -5608,8 +5608,9 @@ - + getSupportedApps + countUsers diff --git a/lib/private/Support/Subscription/Registry.php b/lib/private/Support/Subscription/Registry.php index 3d6a9b09d8..ba9c4099b9 100644 --- a/lib/private/Support/Subscription/Registry.php +++ b/lib/private/Support/Subscription/Registry.php @@ -28,13 +28,16 @@ declare(strict_types=1); namespace OC\Support\Subscription; +use OC\User\Backend; use OCP\AppFramework\QueryException; use OCP\IConfig; use OCP\IServerContainer; +use OCP\IUserManager; use OCP\Support\Subscription\Exception\AlreadyRegisteredException; use OCP\Support\Subscription\IRegistry; use OCP\Support\Subscription\ISubscription; use OCP\Support\Subscription\ISupportedApps; +use Psr\Log\LoggerInterface; class Registry implements IRegistry { @@ -49,10 +52,19 @@ class Registry implements IRegistry { /** @var IServerContainer */ private $container; + /** @var IUserManager */ + private $userManager; + /** @var LoggerInterface */ + private $logger; - public function __construct(IConfig $config, IServerContainer $container) { + public function __construct(IConfig $config, + IServerContainer $container, + IUserManager $userManager, + LoggerInterface $logger) { $this->config = $config; $this->container = $container; + $this->userManager = $userManager; + $this->logger = $logger; } private function getSubscription(): ?ISubscription { @@ -127,9 +139,76 @@ class Registry implements IRegistry { * @since 17.0.0 */ public function delegateHasExtendedSupport(): bool { - if ($this->getSubscription() instanceof ISubscription && method_exists($this->subscription, 'hasExtendedSupport')) { + if ($this->getSubscription() instanceof ISubscription) { return $this->getSubscription()->hasExtendedSupport(); } return false; } + + + /** + * Indicates if a hard user limit is reached and no new users should be created + * + * @since 21.0.0 + */ + public function delegateIsHardUserLimitReached(): bool { + $subscription = $this->getSubscription(); + if ($subscription instanceof ISubscription && + $subscription->hasValidSubscription()) { + $userLimitReached = $subscription->isHardUserLimitReached(); + if ($userLimitReached) { + $this->notifyAboutReachedUserLimit(); + } + return $userLimitReached; + } + + $isOneClickInstance = $this->config->getSystemValueBool('one-click-instance', false); + + if (!$isOneClickInstance) { + return false; + } + + $userCount = $this->getUserCount(); + $hardUserLimit = $this->config->getSystemValue('one-click-instance.user-limit', 50); + + $userLimitReached = $userCount >= $hardUserLimit; + if ($userLimitReached) { + $this->notifyAboutReachedUserLimit(); + } + return $userLimitReached; + } + + private function getUserCount(): int { + $userCount = 0; + $backends = $this->userManager->getBackends(); + foreach ($backends as $backend) { + if ($backend->implementsActions(Backend::COUNT_USERS)) { + $backendUsers = $backend->countUsers(); + if ($backendUsers !== false) { + $userCount += $backendUsers; + } else { + // TODO what if the user count can't be determined? + $this->logger->warning('Can not determine user count for ' . get_class($backend), ['app' => 'lib']); + } + } + } + + $disabledUsers = $this->config->getUsersForUserValue('core', 'enabled', 'false'); + $disabledUsersCount = count($disabledUsers); + $userCount = $userCount - $disabledUsersCount; + + if ($userCount < 0) { + $userCount = 0; + + // this should never happen + $this->logger->warning("Total user count was negative (users: $userCount, disabled: $disabledUsersCount)", ['app' => 'lib']); + } + + return $userCount; + } + + private function notifyAboutReachedUserLimit() { + // TODO notify admin about reached user limit + $this->logger->warning('The user limit was reached and the new user was not created', ['app' => 'lib']); + } } diff --git a/lib/private/User/Manager.php b/lib/private/User/Manager.php index eceeed2050..cf40fc19dd 100644 --- a/lib/private/User/Manager.php +++ b/lib/private/User/Manager.php @@ -34,6 +34,7 @@ namespace OC\User; +use OC\HintException; use OC\Hooks\PublicEmitter; use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\EventDispatcher\IEventDispatcher; @@ -42,6 +43,7 @@ use OCP\IGroup; use OCP\IUser; use OCP\IUserBackend; use OCP\IUserManager; +use OCP\Support\Subscription\IRegistry; use OCP\User\Backend\IGetRealUIDBackend; use OCP\User\Events\CreateUserEvent; use OCP\User\Events\UserCreatedEvent; @@ -297,6 +299,12 @@ class Manager extends PublicEmitter implements IUserManager { * @return bool|IUser the created user or false */ 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()) { + $l = \OC::$server->getL10N('lib'); + throw new HintException($l->t('The user limit has been reached and the user was not created.')); + } + $localBackends = []; foreach ($this->backends as $backend) { if ($backend instanceof Database) { diff --git a/lib/public/Support/Subscription/IRegistry.php b/lib/public/Support/Subscription/IRegistry.php index f3d6cf2f92..ab1d00fa82 100644 --- a/lib/public/Support/Subscription/IRegistry.php +++ b/lib/public/Support/Subscription/IRegistry.php @@ -78,4 +78,11 @@ interface IRegistry { * @since 17.0.0 */ public function delegateHasExtendedSupport(): bool; + + /** + * Indicates if a hard user limit is reached and no new users should be created + * + * @since 21.0.0 + */ + public function delegateIsHardUserLimitReached(): bool; } diff --git a/lib/public/Support/Subscription/ISubscription.php b/lib/public/Support/Subscription/ISubscription.php index 83b7509b9f..9614c0ed77 100644 --- a/lib/public/Support/Subscription/ISubscription.php +++ b/lib/public/Support/Subscription/ISubscription.php @@ -45,4 +45,11 @@ interface ISubscription { * @since 17.0.0 */ public function hasExtendedSupport(): bool; + + /** + * Indicates if a hard user limit is reached and no new users should be created + * + * @since 21.0.0 + */ + public function isHardUserLimitReached(): bool; } diff --git a/tests/lib/Support/Subscription/DummySubscription.php b/tests/lib/Support/Subscription/DummySubscription.php index e1f7f5c6b6..f4e7e3484b 100644 --- a/tests/lib/Support/Subscription/DummySubscription.php +++ b/tests/lib/Support/Subscription/DummySubscription.php @@ -30,6 +30,8 @@ class DummySubscription implements \OCP\Support\Subscription\ISubscription { private $hasValidSubscription; /** @var bool */ private $hasExtendedSupport; + /** @var bool */ + private $isHardUserLimitReached; /** * DummySubscription constructor. @@ -37,9 +39,10 @@ class DummySubscription implements \OCP\Support\Subscription\ISubscription { * @param bool $hasValidSubscription * @param bool $hasExtendedSupport */ - public function __construct(bool $hasValidSubscription, bool $hasExtendedSupport) { + public function __construct(bool $hasValidSubscription, bool $hasExtendedSupport, bool $isHardUserLimitReached) { $this->hasValidSubscription = $hasValidSubscription; $this->hasExtendedSupport = $hasExtendedSupport; + $this->isHardUserLimitReached = $isHardUserLimitReached; } /** @@ -55,4 +58,8 @@ class DummySubscription implements \OCP\Support\Subscription\ISubscription { public function hasExtendedSupport(): bool { return $this->hasExtendedSupport; } + + public function isHardUserLimitReached(): bool { + return $this->isHardUserLimitReached; + } } diff --git a/tests/lib/Support/Subscription/RegistryTest.php b/tests/lib/Support/Subscription/RegistryTest.php index c070f69ae6..58995afcab 100644 --- a/tests/lib/Support/Subscription/RegistryTest.php +++ b/tests/lib/Support/Subscription/RegistryTest.php @@ -25,9 +25,11 @@ namespace Test\Support\Subscription; use OC\Support\Subscription\Registry; use OCP\IConfig; use OCP\IServerContainer; +use OCP\IUserManager; use OCP\Support\Subscription\ISubscription; use OCP\Support\Subscription\ISupportedApps; use PHPUnit\Framework\MockObject\MockObject; +use Psr\Log\LoggerInterface; use Test\TestCase; class RegistryTest extends TestCase { @@ -41,12 +43,25 @@ class RegistryTest extends TestCase { /** @var MockObject|IServerContainer */ private $serverContainer; + /** @var MockObject|IUserManager */ + private $userManager; + + /** @var MockObject|LoggerInterface */ + private $logger; + protected function setUp(): void { parent::setUp(); $this->config = $this->createMock(IConfig::class); $this->serverContainer = $this->createMock(IServerContainer::class); - $this->registry = new Registry($this->config, $this->serverContainer); + $this->userManager = $this->createMock(IUserManager::class); + $this->logger = $this->createMock(LoggerInterface::class); + $this->registry = new Registry( + $this->config, + $this->serverContainer, + $this->userManager, + $this->logger + ); } /** @@ -121,10 +136,74 @@ class RegistryTest extends TestCase { public function testSubscriptionService() { $this->serverContainer->method('query') ->with(DummySubscription::class) - ->willReturn(new DummySubscription(true, false)); + ->willReturn(new DummySubscription(true, false, false)); $this->registry->registerService(DummySubscription::class); $this->assertTrue($this->registry->delegateHasValidSubscription()); $this->assertFalse($this->registry->delegateHasExtendedSupport()); } + + public function testDelegateIsHardUserLimitReached() { + /* @var ISubscription|\PHPUnit\Framework\MockObject\MockObject $subscription */ + $subscription = $this->createMock(ISubscription::class); + $subscription->expects($this->once()) + ->method('hasValidSubscription') + ->willReturn(true); + $subscription->expects($this->once()) + ->method('isHardUserLimitReached') + ->willReturn(true); + $this->registry->register($subscription); + + $this->assertSame(true, $this->registry->delegateIsHardUserLimitReached()); + } + + public function testDelegateIsHardUserLimitReachedWithoutSupportApp() { + $this->config->expects($this->once()) + ->method('getSystemValueBool') + ->with('one-click-instance') + ->willReturn(false); + + $this->assertSame(false, $this->registry->delegateIsHardUserLimitReached()); + } + + public function dataForUserLimitCheck() { + return [ + // $userLimit, $userCount, $disabledUsers, $expectedResult + [35, 15, 2, false], + [35, 45, 15, false], + [35, 45, 5, true], + [35, 45, 55, false], + ]; + } + + /** + * @dataProvider dataForUserLimitCheck + */ + public function testDelegateIsHardUserLimitReachedWithoutSupportAppAndUserCount($userLimit, $userCount, $disabledUsers, $expectedResult) { + $this->config->expects($this->once()) + ->method('getSystemValueBool') + ->with('one-click-instance') + ->willReturn(true); + $this->config->expects($this->once()) + ->method('getSystemValue') + ->with('one-click-instance.user-limit') + ->willReturn($userLimit); + $this->config->expects($this->once()) + ->method('getUsersForUserValue') + ->with('core', 'enabled', 'false') + ->willReturn(array_fill(0, $disabledUsers, '')); + /* @var UserInterface|\PHPUnit\Framework\MockObject\MockObject $dummyBackend */ + $dummyBackend = $this->createMock(UserInterface::class); + $dummyBackend->expects($this->once()) + ->method('implementsActions') + ->willReturn(true); + $dummyBackend->expects($this->once()) + ->method('countUsers') + ->willReturn($userCount); + $this->userManager->expects($this->once()) + ->method('getBackends') + ->willReturn([$dummyBackend]); + + $this->assertSame($expectedResult, $this->registry->delegateIsHardUserLimitReached()); + } }